gsu: Initial completion support.
[gsu.git] / misc / gsu / 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 # Each line matching this is recognized as a subcommand. The name
15 # of the subcommand is the first subexpression.
16 export gsu_command_regex='^com_\([-a-zA-Z_0-9]\+\)()'
17
18 _gsu_available_commands()
19 {
20         result="$({
21                 printf "help\nman\nprefs\ncomplete\n"
22                 sed -ne "s/$gsu_command_regex/\1/g;T;p" $0
23                 } | sort | tr '\n' ' ')"
24 }
25
26 _gsu_print_available_commands()
27 {(
28         local i count
29         gsu_short_msg "Available commands:"
30         for i in $gsu_cmds; do
31                 printf "$i"
32                 count=$(($count + 1))
33                 if test $(($count % 4)) -eq 0; then
34                         echo
35                 else
36                         printf "\t"
37                         if test ${#i} -lt 8; then
38                                 printf "\t"
39                         fi
40                 fi
41         done
42         echo
43 ) 2>&1
44 }
45
46 gsu_complete_options()
47 {
48         local opts="$1" cword="$2" cur
49         local -a words
50
51         shift 2
52         words=("$@")
53         cur="${words[$cword]}"
54         ret=0
55         [[ ! "$cur" == -* ]] && return
56
57         ret=0
58         for ((i=0; i < ${#opts}; i++)); do
59                 opt="${opts:$i:1}"
60                 [[ "$opt" == ":" ]] && continue
61                 printf "%s" "-$opt "
62                 let ret++
63         done
64 }
65
66 export gsu_prefs_txt="
67 Print the current preferences.
68
69 Usage: prefs [-e]
70
71 If -e is given, the config file is opened with the default editor.  Without
72 options, the command prints out a list of all cmt config variables, together
73 with their current value and the default value."
74 _com_prefs()
75 {
76         local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}"
77
78         if [[ "$1" = "-e" ]]; then
79                 ret=-$E_GSU_MKDIR
80                 result="${conf%/*}"
81                 mkdir -p "$result"
82                 [[ $? -ne 0 ]] && return
83                 ret=-$E_GSU_EDITOR
84                 result="${EDITOR:-vi}"
85                 "$result" "$conf"
86                 [[ $? -ne 0 ]] && return
87                 ret=$GSU_SUCCESS
88                 return
89         fi
90
91         for ((i=0; i < ${#gsu_options[@]}; i++)); do
92                 local name= option_type= default_value= required=
93                 local description= help_text=
94                 eval "${gsu_options[$i]}"
95                 eval val='"$'${gsu_config_var_prefix}_$name'"'
96                 case "$required" in
97                 true|yes)
98                         printf "# required"
99                         ;;
100                 *)
101                         printf "# optional"
102                         ;;
103                 esac
104                 printf " $option_type: $description"
105                 if [[ "$required" != "yes" && "$required" != "true" ]]; then
106                         printf " [$default_value]"
107                 fi
108                 echo
109                 [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
110                 printf "$name=$val"
111                 [[ "$val" == "$default_value" ]] && printf " # default"
112                 echo
113         done
114 }
115
116 complete_prefs()
117 {
118         gsu_complete_options "e" "$@"
119 }
120
121 export gsu_man_txt="
122 Print the manual.
123
124 Usage: man"
125
126 _com_man()
127 {
128         local equal_signs="=================================================="
129         local minus_signs="--------------------------------------------------"
130         local com num
131
132         echo "$_gsu_self (_${gsu_banner_txt}_) manual"
133         echo "${equal_signs:0:${#_gsu_self} + ${#gsu_banner_txt} + 16}"
134         echo
135
136         sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
137         echo "----"
138         echo
139         echo "$_gsu_self usage"
140         echo "${minus_signs:0:${#_gsu_self} + 6}"
141         printf "\t"
142         _gsu_usage 2>&1
143         echo "Each command has its own set of options as described below."
144         echo
145         echo "----"
146         echo
147         echo "Available commands:"
148
149         _gsu_available_commands
150         for com in $result; do
151                 num=${#com}
152                 if test $num -lt 4; then
153                         num=4
154                 fi
155                 echo "${minus_signs:0:$num}"
156                 echo "$com"
157                 echo "${minus_signs:0:$num}"
158                 $0 help $com
159                 echo
160         done
161         ret=$GSU_SUCCESS
162 }
163
164 _gsu_banner_msg()
165 {
166         local txt="### $_gsu_self --"
167         if test -z "$gsu_banner_txt"; then
168                 txt="$txt set \$gsu_banner_txt to customize this message"
169         else
170                 txt="$txt $gsu_banner_txt"
171         fi
172         gsu_short_msg "$txt ###"
173 }
174
175 export gsu_help_txt="
176 Print online help.
177
178 Usage: help [command]
179
180 Without arguments, print the list of available commands. Otherwise,
181 print the help text for the given command."
182
183 export gsu_complete_txt="
184 Command line completion.
185
186 Usage: complete [<cword> <word>...]
187
188 In the first form, the command prints all possible completions to stdout.
189 This can be used from the completion function of the shell.
190
191 Completion code suitable to be evaled is written to stdout if no argument
192 was given.
193 "
194
195 _com_help()
196 {
197         local a b
198         if test -z "$1"; then
199                 _gsu_banner_msg 2>&1
200                 _gsu_usage 2>&1
201                 {
202                         printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
203                         printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
204                         printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
205                         printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
206                         grep -A 2 "$gsu_command_regex" $0
207                 } | grep -v -- '--' \
208                         | sed -e "/$gsu_command_regex/bs" \
209                                 -e 'H;$!d;x;s/\n//g;b' \
210                                 -e :s \
211                                 -e 'x;s/\n//g;${p;x;}' \
212                         | sed -e "s/${gsu_command_regex}#*/\1\t/" \
213                         | sort \
214                         | while read a b; do
215                                 printf "$a\t"
216                                 if test ${#a} -lt 8; then
217                                         printf "\t"
218                                 fi
219                                 echo "$b"
220                         done
221                 echo
222                 echo "# Try $_gsu_self help <command> for info on <command>."
223                 ret=$GSU_SUCCESS
224                 return
225         fi
226         if test "$1" = "help"; then
227                 echo "$gsu_help_txt"
228                 ret=$GSU_SUCCESS
229                 return
230         fi
231         if test "$1" = "man"; then
232                 echo "$gsu_man_txt"
233                 ret=$GSU_SUCCESS
234                 return
235         fi
236         if test "$1" = "prefs"; then
237                 echo "$gsu_prefs_txt"
238                 ret=$GSU_SUCCESS
239                 return
240         fi
241         if test "$1" = "complete"; then
242                 echo "$gsu_complete_txt"
243                 ret=$GSU_SUCCESS
244                 return
245         fi
246         ret=$GSU_SUCCESS
247         if grep -q "^com_$1()" $0; then
248                 sed -e "1,/^com_$1()$/d" -e '/^{/,$d' -e 's/^## *//' $0
249                 return
250         fi
251         _gsu_print_available_commands
252         result="$1"
253         ret=-$E_GSU_BAD_COMMAND
254 }
255
256 complete_help()
257 {
258         _gsu_available_commands
259         echo "$result"
260 }
261
262 # Wrapper for bash's getopts.
263 #
264 # Aborts on programming errors such as missing or invalid option string.  On
265 # success $result contains shell code that can be eval'ed. For each defined
266 # option x, the local variable o_x will be created when calling eval "$result".
267 # o_x contains true/false for options without an argument or the emtpy string/the
268 # given argument, depending on whether this option was contained in the "$@"
269 # array.
270 #
271 # Example:
272 #       gsu_getopts abc:x:y
273 #       eval "$result"
274 #       [[ $ret -lt 0 ]] && return
275 #
276 #       [[ "$o_a" = "true ]] && echo "The -a flag was given"
277 #       [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
278 gsu_getopts()
279 {
280         local i c tab=' ' cr='
281 '
282
283         gsu_check_arg_count $# 1 1
284         if [[ $ret -lt 0 ]]; then
285                 gsu_err_msg
286                 exit 1
287         fi
288
289         ret=-$E_GSU_GETOPTS
290         result="invalid optstring $1"
291         if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
292                 gsu_err_msg
293                 exit 1
294         fi
295
296         for ((i=0; i < ${#1}; i++)); do
297                 c=${1:$i:1}
298                 case "$c" in
299                 [a-zA-Z:]);;
300                 *)
301                         ret=-$E_GSU_GETOPTS
302                         result="invalid character $c in optstring"
303                         gsu_err_msg
304                         exit 1
305                 esac
306         done
307         result="local opt"
308         for ((i=0; i < ${#1}; i++)); do
309                 c1=${1:$i:1}
310                 c2=${1:$(($i + 1)):1}
311                 result+=" o_$c1"
312                 if [[ "$c2" = ":" ]]; then
313                         let i++
314                 else
315                         result+="=false"
316                 fi
317         done
318         result+="
319         OPTIND=1
320         while getopts $1 opt \"\$@\"; do
321                 case \"\$opt\" in
322 "
323         for ((i=0; i < ${#1}; i++)); do
324                 c1=${1:$i:1}
325                 c2=${1:$(($i + 1)):1}
326                 result+="$tab$tab$c1) o_$c1="
327                 if [[ "$c2" = ":" ]]; then
328                         result+="\"\$OPTARG\""
329                         let i++
330                 else
331                         result+="true"
332                 fi
333                 result+=";;$cr"
334         done
335         result+="
336                 *)
337                         ret=-\$E_GSU_GETOPTS
338                         result=\"invalid option given\"
339                         return
340                         ;;
341                 esac
342         done
343         shift \$((\$OPTIND - 1))
344 "
345         ret=$GSU_SUCCESS
346 }
347
348 _com_complete()
349 {
350         local cmd n cword="$1"
351         local -a words
352
353         if (($# == 0)); then
354                 cat <<EOF
355                 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
356                 local -a candidates;
357
358                 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
359                 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
360 EOF
361         fi
362
363         [[ -z "$cword" ]] && return
364         if (($cword <= 1)); then
365                 _gsu_available_commands
366                 echo "${result}"
367                 ret=$GSU_SUCCESS
368                 return
369         fi
370         shift
371         words=("$@")
372         cmd="${words[1]}"
373         ret=$GSU_SUCCESS # It's not an error if no completer was defined
374         [[ "$(type -t complete_$cmd)" != "function" ]] && return
375         complete_$cmd "$cword" "${words[@]}"
376         # ignore errors, they would only clutter the completion output
377         ret=$GSU_SUCCESS
378 }
379
380 gsu_cword_is_option_parameter()
381 {
382         local opts="$1" cword="$2" prev i n
383         local -a words
384
385         result=
386         (($cword == 0)) && return
387         ((${#opts} < 2)) && return
388
389         shift 2
390         words=("$@")
391         prev="${words[$(($cword - 1))]}"
392         [[ ! "$prev" == -* ]] && return
393
394         n=$((${#opts} - 1))
395         for ((i=0; i < $n; i++)); do
396                 opt="${opts:$i:1}"
397                 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
398                 let i++
399                 [[ "$prev" != "-$opt" ]] && continue
400                 result="$opt"
401                 return
402         done
403         ret=0
404 }
405
406 gsu()
407 {
408         local i
409         _gsu_setup
410         _gsu_available_commands
411         gsu_cmds="$result"
412         if test $# -eq 0; then
413                 _gsu_usage
414                 _gsu_print_available_commands
415                 exit 1
416         fi
417         arg="$1"
418         shift
419         # check internal commands
420         if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then
421                 _com_$arg "$@"
422                 if [[ "$ret" -lt 0 ]]; then
423                         gsu_err_msg
424                         exit 1
425                 fi
426                 exit 0
427         fi
428
429         # external commands
430         for i in $gsu_cmds; do
431                 if test "$arg" = "$i"; then
432                         com_$arg "$@"
433                         if [[ "$ret" -lt 0 ]]; then
434                                 gsu_err_msg
435                                 exit 1
436                         fi
437                         exit 0
438                 fi
439         done
440
441         ret=-$E_GSU_BAD_COMMAND
442         result="$arg"
443         gsu_err_msg
444         _gsu_print_available_commands
445         exit 1
446 }
447
448 # Check number of arguments.
449 #
450 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
451 #
452 # Check that <num_given> is between <num1> and <num2> inclusively.
453 # If only <num1> ist given, num2 is assumed to be infinity.
454 #
455 # Examples:
456 #       0 0 no argument allowed
457 #       1 1 exactly one argument required
458 #       0 2 at most two arguments admissible
459 #       2   at least two arguments reqired
460 #
461 gsu_check_arg_count()
462 {
463         ret=-$E_GSU_BAD_ARG_COUNT
464         if [[ $# -eq 2 ]]; then # only num1 is given
465                 result="at least $2 args required, $1 given"
466                 [[ $1 -lt $2 ]] && return
467                 ret=$GSU_SUCCESS
468                 return
469         fi
470         # num1 and num2 given
471         result="need at least $2 args, $1 given"
472         [[ $1 -lt $2 ]] && return
473         result="need at most $3 args, $1 given"
474         [[ $1 -gt $3 ]] && return
475         ret=$GSU_SUCCESS
476 }