32ca366aef022737089f8736d99bab9e5721d24f
[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 _com_prefs()
104 {
105 local i conf="${gsu_config_file:=${HOME:-}/.$gsu_name.rc}"
106
107 gsu_getopts "e"
108 eval "$result"
109 (($ret < 0)) && return
110 gsu_check_arg_count $# 0 0
111 (($ret < 0)) && return
112
113 if [[ "$o_e" == "true" ]]; then
114 ret=-$E_GSU_MKDIR
115 result="${conf%/*}"
116 mkdir -p "$result"
117 (($? != 0)) && return
118 ret=-$E_GSU_EDITOR
119 result="${EDITOR:-vi}"
120 "$result" "$conf"
121 (($? != 0)) && return
122 ret=$GSU_SUCCESS
123 return
124 fi
125
126 for ((i=0; i < ${#gsu_options[@]}; i++)); do
127 local name= option_type= default_value= required=
128 local description= help_text=
129 eval "${gsu_options[$i]}"
130 eval val='"${'${gsu_config_var_prefix}_$name:-'}"'
131 case "$required" in
132 true|yes)
133 printf "# required"
134 ;;
135 *)
136 printf "# optional"
137 ;;
138 esac
139 printf " $option_type: $description"
140 if [[ "$required" != "yes" && "$required" != "true" ]]; then
141 printf " [$default_value]"
142 fi
143 echo
144 [[ -n "$help_text" ]] && sed -e '/^[ ]*$/d; s/^[ ]*/# /g' <<< "$help_text"
145 printf "$name=$val"
146 [[ "$val" == "$default_value" ]] && printf " # default"
147 echo
148 done
149 }
150
151 complete_prefs()
152 {
153 gsu_complete_options "e" "$@"
154 }
155
156 export gsu_man_txt="
157 Print the manual.
158
159 Usage: man"
160
161 _com_man()
162 {
163 local equal_signs="=================================================="
164 local minus_signs="--------------------------------------------------"
165 local com num
166
167 echo "$gsu_name (_${gsu_banner_txt}_) manual"
168 echo "${equal_signs:0:${#gsu_name} + ${#gsu_banner_txt} + 16}"
169 echo
170
171 sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
172 echo "----"
173 echo
174 echo "$gsu_name usage"
175 echo "${minus_signs:0:${#gsu_name} + 6}"
176 printf "\t"
177 _gsu_usage 2>&1
178 echo "Each command has its own set of options as described below."
179 echo
180 echo "----"
181 echo
182 echo "Available commands:"
183
184 _gsu_available_commands
185 for com in $result; do
186 num=${#com}
187 (($num < 4)) && num=4
188 echo "${minus_signs:0:$num}"
189 echo "$com"
190 echo "${minus_signs:0:$num}"
191 $0 help $com
192 echo
193 done
194 ret=$GSU_SUCCESS
195 }
196
197 export gsu_help_txt="
198 Print online help.
199
200 Usage: help [command]
201
202 Without arguments, print the list of available commands. Otherwise,
203 print the help text for the given command."
204
205 export gsu_complete_txt="
206 Command line completion.
207
208 Usage: complete [<cword> <word>...]
209
210 When executed without argument the command writes bash code to
211 stdout. This code is suitable to be evaled from .bashrc to enable
212 completion.
213
214 If at least one argument is given, all possible completions are
215 written to stdout. This can be used from the completion function of
216 the subcommand.
217 "
218
219 _com_help()
220 {
221 local a b ere tab=' '
222
223 _gsu_get_command_regex
224 ere="$result"
225
226 if (($# == 0)); then
227 gsu_short_msg "### $gsu_name -- $gsu_banner_txt ###"
228 _gsu_usage 2>&1
229 {
230 printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
231 printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
232 printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
233 printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
234 grep -EA 2 "$ere" $0
235 } | grep -v -- '--' \
236 | sed -En "/$ere/"'!d
237 # remove everything but the command name
238 s/^com_(.*)\(\).*/\1/
239
240 # append tab after short commands (less than 8 chars)
241 s/^(.{1,7})$/\1'"$tab"'/g
242
243 # remove next line (should contain only ## anyway)
244 N
245 s/#.*//
246
247 # append next line, removing leading ##
248 N
249 s/#+ *//g
250
251 # replace newline by tab
252 y/\n/'"$tab"'/
253
254 # and print the sucker
255 p'
256 echo
257 echo "# Try $gsu_name help <command> for info on <command>."
258 ret=$GSU_SUCCESS
259 return
260 fi
261 if test "$1" = "help"; then
262 echo "$gsu_help_txt"
263 ret=$GSU_SUCCESS
264 return
265 fi
266 if test "$1" = "man"; then
267 echo "$gsu_man_txt"
268 ret=$GSU_SUCCESS
269 return
270 fi
271 if test "$1" = "prefs"; then
272 echo "$gsu_prefs_txt"
273 ret=$GSU_SUCCESS
274 return
275 fi
276 if test "$1" = "complete"; then
277 echo "$gsu_complete_txt"
278 ret=$GSU_SUCCESS
279 return
280 fi
281 ret=$GSU_SUCCESS
282 _gsu_get_command_regex "$1"
283 ere="$result"
284 if ! grep -Eq "$ere" $0; then
285 _gsu_print_available_commands
286 result="$1"
287 ret=-$E_GSU_BAD_COMMAND
288 return
289 fi
290 sed -nEe '
291 # only consider lines in the comment of the function
292 /'"$ere"'/,/^[^#]/ {
293
294 # remove leading ##
295 s/^## *//
296
297 # if it did start with ##, jump to label p and print it
298 tp
299
300 # otherwise, move on to next line
301 d
302
303 # print it
304 :p
305 p
306 }
307 ' $0
308 }
309
310 complete_help()
311 {
312 _gsu_available_commands
313 echo "$result"
314 }
315
316 # Wrapper for the bash getopts builtin.
317 #
318 # Aborts on programming errors such as missing or invalid option string. On
319 # success $result contains shell code that can be eval'ed. For each defined
320 # option x, the local variable o_x will be created when calling eval "$result".
321 # o_x contains true/false for options without argument and either the empty
322 # string or the given argument for options that take an argument.
323 #
324 # Example:
325 # gsu_getopts abc:x:y
326 # eval "$result"
327 # (($ret < 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 < 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 # Find out if the current word is a parameter for an option.
438 #
439 # $1: usual getopts option string.
440 # $2: The current word number.
441 # $3..: All words of the current command line.
442 #
443 # return: If yes, $result contains the letter of the option for which the
444 # current word is a parameter. Otherwise, $result is empty.
445 #
446 gsu_cword_is_option_parameter()
447 {
448 local opts="$1" cword="$2" prev i n
449 local -a words
450
451 result=
452 (($cword == 0)) && return
453 ((${#opts} < 2)) && return
454
455 shift 2
456 words=("$@")
457 prev="${words[$(($cword - 1))]}"
458 [[ ! "$prev" == -* ]] && return
459
460 n=$((${#opts} - 1))
461 for ((i=0; i <= $n; i++)); do
462 opt="${opts:$i:1}"
463 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
464 let i++
465 [[ "$prev" != "-$opt" ]] && continue
466 result="$opt"
467 return
468 done
469 ret=0
470 }
471
472 # Get the word number on which the cursor is, not counting options.
473 #
474 # This is useful for completing commands whose possible completions depend
475 # on the word number, for example mount.
476 #
477 # $1: Getopt option string.
478 # $2: The current word number.
479 # $3..: All words of the current command line.
480 #
481 # return: If the current word is an option, or a parameter to an option,
482 # this function sets $result to -1. Otherwise, the number of the non-option
483 # is returned in $result.
484 #
485 gsu_get_unnamed_arg_num()
486 {
487 local opts="$1" cword="$2" prev cur
488 local -i i n=0
489 local -a words
490
491 shift 2
492 words=("$@")
493 cur="${words[$cword]}"
494 prev="${words[$(($cword - 1))]}"
495 result=-1
496 [[ "$cur" == -* ]] && return
497 [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
498
499 for ((i=1; i <= $cword; i++)); do
500 prev="${words[$(($i - 1))]}"
501 cur="${words[$i]}"
502 [[ "$cur" == -* ]] && continue
503 if [[ "$prev" == -* ]]; then
504 opt=${prev#-}
505 [[ "$opts" != *$opt:* ]] && let n++
506 continue
507 fi
508 let n++
509 done
510 result="$(($n - 1))"
511 }
512
513 # Entry point for all gsu-based scripts.
514 #
515 # The startup part of the application script should source this file to load
516 # the functions defined here, and then call gsu(). Functions starting with com_
517 # are automatically recognized as subcommands.
518 #
519 # Minimal example:
520 #
521 # com_hello()
522 # {
523 # echo 'hello world'
524 # }
525 # gsu_dir=${gsu_dir:-/system/location/where/gsu/is/installed}
526 # . $gsu_dir/subcommand || exit 1
527 # gsu "$@"
528 gsu()
529 {
530 local i
531 _gsu_available_commands
532 gsu_cmds="$result"
533 if (($# == 0)); then
534 _gsu_usage
535 _gsu_print_available_commands
536 exit 1
537 fi
538 arg="$1"
539 shift
540 # check internal commands
541 if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then
542 _com_$arg "$@"
543 if (("$ret" < 0)); then
544 gsu_err_msg
545 exit 1
546 fi
547 exit 0
548 fi
549
550 # external commands
551 for i in $gsu_cmds; do
552 if test "$arg" = "$i"; then
553 ret=$GSU_SUCCESS
554 com_$arg "$@"
555 if (("$ret" < 0)); then
556 gsu_err_msg
557 exit 1
558 fi
559 exit 0
560 fi
561 done
562
563 ret=-$E_GSU_BAD_COMMAND
564 result="$arg"
565 gsu_err_msg
566 _gsu_print_available_commands
567 exit 1
568 }
569
570 # Check number of arguments.
571 #
572 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
573 #
574 # Check that <num_given> is between <num1> and <num2> inclusively.
575 # If only <num1> ist given, num2 is assumed to be infinity.
576 #
577 # Examples:
578 # 0 0 no argument allowed
579 # 1 1 exactly one argument required
580 # 0 2 at most two arguments admissible
581 # 2 at least two arguments required
582 gsu_check_arg_count()
583 {
584 ret=-$E_GSU_BAD_ARG_COUNT
585 if (($# == 2)); then # only num1 is given
586 result="at least $2 args required, $1 given"
587 (($1 < $2)) && return
588 ret=$GSU_SUCCESS
589 return
590 fi
591 # num1 and num2 given
592 result="need at least $2 args, $1 given"
593 (($1 < $2)) && return
594 result="need at most $3 args, $1 given"
595 (($1 > $3)) && return
596 ret=$GSU_SUCCESS
597 }