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