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