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