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