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