Fix gsu com_complete().
[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
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 local txt="### $_gsu_self --"
190 if test -z "$gsu_banner_txt"; then
191 txt="$txt set \$gsu_banner_txt to customize this message"
192 else
193 txt="$txt $gsu_banner_txt"
194 fi
195 gsu_short_msg "$txt ###"
196 }
197
198 export gsu_help_txt="
199 Print online help.
200
201 Usage: help [command]
202
203 Without arguments, print the list of available commands. Otherwise,
204 print the help text for the given command."
205
206 export gsu_complete_txt="
207 Command line completion.
208
209 Usage: complete [<cword> <word>...]
210
211 In the first form, the command prints all possible completions to stdout.
212 This can be used from the completion function of the shell.
213
214 Completion code suitable to be evaled is written to stdout if no argument
215 was given.
216 "
217
218 _com_help()
219 {
220 local a b ere tab=' '
221
222 _gsu_get_command_regex
223 ere="$result"
224
225 if (($# == 0)); then
226 _gsu_banner_msg 2>&1
227 _gsu_usage 2>&1
228 {
229 printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
230 printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
231 printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
232 printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
233 grep -EA 2 "$ere" $0
234 } | grep -v -- '--' \
235 | sed -En "/$ere/"'!d
236 # remove everything but the command name
237 s/^com_(.*)\(\).*/\1/
238
239 # append tab after short commands (less than 8 chars)
240 s/^(.{1,7})$/\1'"$tab"'/g
241
242 # remove next line (should contain only ## anyway)
243 N
244 s/#.*//
245
246 # append next line, removing leading ##
247 N
248 s/#+ *//g
249
250 # replace newline by tab
251 y/\n/'"$tab"'/
252
253 # and print the sucker
254 p'
255 echo
256 echo "# Try $_gsu_self help <command> for info on <command>."
257 ret=$GSU_SUCCESS
258 return
259 fi
260 if test "$1" = "help"; then
261 echo "$gsu_help_txt"
262 ret=$GSU_SUCCESS
263 return
264 fi
265 if test "$1" = "man"; then
266 echo "$gsu_man_txt"
267 ret=$GSU_SUCCESS
268 return
269 fi
270 if test "$1" = "prefs"; then
271 echo "$gsu_prefs_txt"
272 ret=$GSU_SUCCESS
273 return
274 fi
275 if test "$1" = "complete"; then
276 echo "$gsu_complete_txt"
277 ret=$GSU_SUCCESS
278 return
279 fi
280 ret=$GSU_SUCCESS
281 _gsu_get_command_regex "$1"
282 ere="$result"
283 if ! grep -Eq "$ere" $0; then
284 _gsu_print_available_commands
285 result="$1"
286 ret=-$E_GSU_BAD_COMMAND
287 return
288 fi
289 sed -nEe '
290 # only consider lines in the comment of the function
291 /'"$ere"'/,/^[^#]/ {
292
293 # remove leading ##
294 s/^## *//
295
296 # if it did start with ##, jump to label p and print it
297 tp
298
299 # otherwise, move on to next line
300 d
301
302 # print it
303 :p
304 p
305 }
306 ' $0
307 }
308
309 complete_help()
310 {
311 _gsu_available_commands
312 echo "$result"
313 }
314
315 # Wrapper for bash's getopts.
316 #
317 # Aborts on programming errors such as missing or invalid option string. On
318 # success $result contains shell code that can be eval'ed. For each defined
319 # option x, the local variable o_x will be created when calling eval "$result".
320 # o_x contains true/false for options without an argument or the emtpy string/the
321 # given argument, depending on whether this option was contained in the "$@"
322 # array.
323 #
324 # Example:
325 # gsu_getopts abc:x:y
326 # eval "$result"
327 # [[ $ret -lt 0 ]] && return
328 #
329 # [[ "$o_a" = "true ]] && echo "The -a flag was given"
330 # [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
331 gsu_getopts()
332 {
333 local i c tab=' ' cr='
334 '
335
336 gsu_check_arg_count $# 1 1
337 if [[ $ret -lt 0 ]]; then
338 gsu_err_msg
339 exit 1
340 fi
341
342 ret=-$E_GSU_GETOPTS
343 result="invalid optstring $1"
344 if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
345 gsu_err_msg
346 exit 1
347 fi
348
349 for ((i=0; i < ${#1}; i++)); do
350 c=${1:$i:1}
351 case "$c" in
352 [a-zA-Z:]);;
353 *)
354 ret=-$E_GSU_GETOPTS
355 result="invalid character $c in optstring"
356 gsu_err_msg
357 exit 1
358 esac
359 done
360 result="local _gsu_getopts_opt"
361 for ((i=0; i < ${#1}; i++)); do
362 c1=${1:$i:1}
363 c2=${1:$(($i + 1)):1}
364 result+=" o_$c1="
365 if [[ "$c2" = ":" ]]; then
366 let i++
367 else
368 result+="false"
369 fi
370 done
371 result+="
372 OPTIND=1
373 while getopts $1 _gsu_getopts_opt \"\$@\"; do
374 case \"\$_gsu_getopts_opt\" in
375 "
376 for ((i=0; i < ${#1}; i++)); do
377 c1=${1:$i:1}
378 c2=${1:$(($i + 1)):1}
379 result+="$tab$tab$c1) o_$c1="
380 if [[ "$c2" = ":" ]]; then
381 result+="\"\$OPTARG\""
382 let i++
383 else
384 result+="true"
385 fi
386 result+=";;$cr"
387 done
388 result+="
389 *)
390 ret=-\$E_GSU_GETOPTS
391 result=\"invalid option given\"
392 return
393 ;;
394 esac
395 done
396 shift \$((\$OPTIND - 1))
397 "
398 ret=$GSU_SUCCESS
399 }
400
401 _com_complete()
402 {
403 local cmd n cword
404 local -a words
405
406 if (($# == 0)); then
407 cat <<EOF
408 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
409 local -a candidates;
410
411 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
412 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
413 EOF
414 ret=$GSU_SUCCESS
415 return
416 fi
417
418 cword="$1"
419 gsu_is_a_number "$cword"
420 (($ret < 0)) && return
421 if (($cword <= 1)); then
422 _gsu_available_commands
423 echo "${result}"
424 ret=$GSU_SUCCESS
425 return
426 fi
427 shift
428 words=("$@")
429 cmd="${words[1]}"
430 ret=$GSU_SUCCESS # It's not an error if no completer was defined
431 [[ "$(type -t complete_$cmd)" != "function" ]] && return
432 complete_$cmd "$cword" "${words[@]}"
433 # ignore errors, they would only clutter the completion output
434 ret=$GSU_SUCCESS
435 }
436
437 gsu_cword_is_option_parameter()
438 {
439 local opts="$1" cword="$2" prev i n
440 local -a words
441
442 result=
443 (($cword == 0)) && return
444 ((${#opts} < 2)) && return
445
446 shift 2
447 words=("$@")
448 prev="${words[$(($cword - 1))]}"
449 [[ ! "$prev" == -* ]] && return
450
451 n=$((${#opts} - 1))
452 for ((i=0; i < $n; i++)); do
453 opt="${opts:$i:1}"
454 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
455 let i++
456 [[ "$prev" != "-$opt" ]] && continue
457 result="$opt"
458 return
459 done
460 ret=0
461 }
462
463 gsu()
464 {
465 local i
466 _gsu_setup
467 _gsu_available_commands
468 gsu_cmds="$result"
469 if test $# -eq 0; then
470 _gsu_usage
471 _gsu_print_available_commands
472 exit 1
473 fi
474 arg="$1"
475 shift
476 # check internal commands
477 if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then
478 _com_$arg "$@"
479 if [[ "$ret" -lt 0 ]]; then
480 gsu_err_msg
481 exit 1
482 fi
483 exit 0
484 fi
485
486 # external commands
487 for i in $gsu_cmds; do
488 if test "$arg" = "$i"; then
489 com_$arg "$@"
490 if [[ "$ret" -lt 0 ]]; then
491 gsu_err_msg
492 exit 1
493 fi
494 exit 0
495 fi
496 done
497
498 ret=-$E_GSU_BAD_COMMAND
499 result="$arg"
500 gsu_err_msg
501 _gsu_print_available_commands
502 exit 1
503 }
504
505 # Check number of arguments.
506 #
507 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
508 #
509 # Check that <num_given> is between <num1> and <num2> inclusively.
510 # If only <num1> ist given, num2 is assumed to be infinity.
511 #
512 # Examples:
513 # 0 0 no argument allowed
514 # 1 1 exactly one argument required
515 # 0 2 at most two arguments admissible
516 # 2 at least two arguments reqired
517 #
518 gsu_check_arg_count()
519 {
520 ret=-$E_GSU_BAD_ARG_COUNT
521 if [[ $# -eq 2 ]]; then # only num1 is given
522 result="at least $2 args required, $1 given"
523 [[ $1 -lt $2 ]] && return
524 ret=$GSU_SUCCESS
525 return
526 fi
527 # num1 and num2 given
528 result="need at least $2 args, $1 given"
529 [[ $1 -lt $2 ]] && return
530 result="need at most $3 args, $1 given"
531 [[ $1 -gt $3 ]] && return
532 ret=$GSU_SUCCESS
533 }