71f06141abbdd7f0f0b81068abb2037190abec99
[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 _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 _gsu_man_txt="
276 Print the manual.
277
278 Usage: man"
279
280 com_man()
281 {
282 local equal_signs="=================================================="
283 local minus_signs="--------------------------------------------------"
284 local com num
285
286 echo "$gsu_name (_${gsu_banner_txt}_) manual"
287 echo "${equal_signs:0:${#gsu_name} + ${#gsu_banner_txt} + 16}"
288 echo
289
290 sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
291 echo "----"
292 echo
293 echo "$gsu_name usage"
294 echo "${minus_signs:0:${#gsu_name} + 6}"
295 printf "\t"
296 _gsu_usage 2>&1
297 echo "Each command has its own set of options as described below."
298 echo
299 echo "----"
300 echo
301 echo "Available commands:"
302
303 _gsu_available_commands
304 for com in $result; do
305 num=${#com}
306 (($num < 4)) && num=4
307 echo "${minus_signs:0:$num}"
308 echo "$com"
309 echo "${minus_signs:0:$num}"
310 $0 help $com
311 echo
312 done
313 ret=$GSU_SUCCESS
314 }
315
316 _gsu_help_txt="
317 Print online help.
318
319 Usage: help [command]
320
321 Without arguments, print the list of available commands. Otherwise,
322 print the help text for the given command."
323
324 _gsu_complete_txt="
325 Command line completion.
326
327 Usage: complete [<cword> <word>...]
328
329 When executed without argument the command writes bash code to
330 stdout. This code is suitable to be evaled from .bashrc to enable
331 completion.
332
333 If at least one argument is given, all possible completions are
334 written to stdout. This can be used from the completion function of
335 the subcommand.
336 "
337
338 com_help()
339 {
340 local a b ere tab=' '
341
342 _gsu_get_command_regex
343 ere="$result"
344
345 if (($# == 0)); then
346 gsu_short_msg "### $gsu_name -- $gsu_banner_txt ###"
347 _gsu_usage 2>&1
348 {
349 printf "com_help()\n$_gsu_help_txt" | head -n 4; echo "--"
350 printf "com_man()\n$_gsu_man_txt" | head -n 4; echo "--"
351 printf "com_prefs()\n$_gsu_prefs_txt" | head -n 4; echo "--"
352 printf "com_complete()\n$_gsu_complete_txt" | head -n 4; echo "--"
353 grep -EA 2 "$ere" $0
354 } | grep -v -- '--' \
355 | sed -En "/$ere/"'!d
356 # remove everything but the command name
357 s/^com_(.*)\(\).*/\1/
358
359 # append tab after short commands (less than 8 chars)
360 s/^(.{1,7})$/\1'"$tab"'/g
361
362 # remove next line (should contain only ## anyway)
363 N
364 s/#.*//
365
366 # append next line, removing leading ##
367 N
368 s/#+ *//g
369
370 # replace newline by tab
371 y/\n/'"$tab"'/
372
373 # and print the sucker
374 p'
375 echo
376 echo "# Try $gsu_name help <command> for info on <command>."
377 ret=$GSU_SUCCESS
378 return
379 fi
380 if test "$1" = "help"; then
381 echo "$_gsu_help_txt"
382 ret=$GSU_SUCCESS
383 return
384 fi
385 if test "$1" = "man"; then
386 echo "$_gsu_man_txt"
387 ret=$GSU_SUCCESS
388 return
389 fi
390 if test "$1" = "prefs"; then
391 echo "$_gsu_prefs_txt"
392 ret=$GSU_SUCCESS
393 return
394 fi
395 if test "$1" = "complete"; then
396 echo "$_gsu_complete_txt"
397 ret=$GSU_SUCCESS
398 return
399 fi
400 ret=$GSU_SUCCESS
401 _gsu_get_command_regex "$1"
402 ere="$result"
403 if ! grep -Eq "$ere" $0; then
404 _gsu_print_available_commands
405 result="$1"
406 ret=-$E_GSU_BAD_COMMAND
407 return
408 fi
409 sed -nEe '
410 # only consider lines in the comment of the function
411 /'"$ere"'/,/^[^#]/ {
412
413 # remove leading ##
414 s/^## *//
415
416 # if it did start with ##, jump to label p and print it
417 tp
418
419 # otherwise, move on to next line
420 d
421
422 # print it
423 :p
424 p
425 }
426 ' $0
427 }
428
429 complete_help()
430 {
431 _gsu_available_commands
432 echo "$result"
433 }
434
435 com_complete()
436 {
437 local cmd n cword
438 local -a words
439
440 if (($# == 0)); then
441 cat <<EOF
442 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
443 local -a candidates;
444
445 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
446 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
447 EOF
448 ret=$GSU_SUCCESS
449 return
450 fi
451
452 cword="$1"
453 gsu_is_a_number "$cword"
454 (($ret < 0)) && return
455 if (($cword <= 1)); then
456 _gsu_available_commands
457 echo "${result}"
458 ret=$GSU_SUCCESS
459 return
460 fi
461 shift
462 words=("$@")
463 cmd="${words[1]}"
464 ret=$GSU_SUCCESS # It's not an error if no completer was defined
465 [[ "$(type -t complete_$cmd)" != "function" ]] && return
466 complete_$cmd "$cword" "${words[@]}"
467 # ignore errors, they would only clutter the completion output
468 ret=$GSU_SUCCESS
469 }
470
471 # Find out if the current word is a parameter for an option.
472 #
473 # $1: usual getopts option string.
474 # $2: The current word number.
475 # $3..: All words of the current command line.
476 #
477 # return: If yes, $result contains the letter of the option for which the
478 # current word is a parameter. Otherwise, $result is empty.
479 #
480 gsu_cword_is_option_parameter()
481 {
482 local opts="$1" cword="$2" prev i n
483 local -a words
484
485 result=
486 (($cword == 0)) && return
487 ((${#opts} < 2)) && return
488
489 shift 2
490 words=("$@")
491 prev="${words[$(($cword - 1))]}"
492 [[ ! "$prev" == -* ]] && return
493
494 n=$((${#opts} - 1))
495 for ((i=0; i <= $n; i++)); do
496 opt="${opts:$i:1}"
497 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
498 let i++
499 [[ "$prev" != "-$opt" ]] && continue
500 result="$opt"
501 return
502 done
503 ret=0
504 }
505
506 # Get the word number on which the cursor is, not counting options.
507 #
508 # This is useful for completing commands whose possible completions depend
509 # on the word number, for example mount.
510 #
511 # $1: Getopt option string.
512 # $2: The current word number.
513 # $3..: All words of the current command line.
514 #
515 # return: If the current word is an option, or a parameter to an option,
516 # this function sets $result to -1. Otherwise, the number of the non-option
517 # is returned in $result.
518 #
519 gsu_get_unnamed_arg_num()
520 {
521 local opts="$1" cword="$2" prev cur
522 local -i i n=0
523 local -a words
524
525 shift 2
526 words=("$@")
527 cur="${words[$cword]}"
528 prev="${words[$(($cword - 1))]}"
529 result=-1
530 [[ "$cur" == -* ]] && return
531 [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
532
533 for ((i=1; i <= $cword; i++)); do
534 prev="${words[$(($i - 1))]}"
535 cur="${words[$i]}"
536 [[ "$cur" == -* ]] && continue
537 if [[ "$prev" == -* ]]; then
538 opt=${prev#-}
539 [[ "$opts" != *$opt:* ]] && let n++
540 continue
541 fi
542 let n++
543 done
544 result="$(($n - 1))"
545 }
546
547 # Entry point for all gsu-based scripts.
548 #
549 # The startup part of the application script should source this file to load
550 # the functions defined here, and then call gsu(). Functions starting with com_
551 # are automatically recognized as subcommands.
552 #
553 # Minimal example:
554 #
555 # com_hello()
556 # {
557 # echo 'hello world'
558 # }
559 # gsu_dir=${gsu_dir:-/system/location/where/gsu/is/installed}
560 # . $gsu_dir/subcommand || exit 1
561 # gsu "$@"
562 gsu()
563 {
564 local i
565
566 if (($# == 0)); then
567 _gsu_usage
568 _gsu_print_available_commands
569 exit 1
570 fi
571 arg="$1"
572 shift
573 if [[ "$(type -t com_$arg)" == 'function' ]]; then
574 com_$arg "$@"
575 if (("$ret" < 0)); then
576 gsu_err_msg
577 exit 1
578 fi
579 exit 0
580 fi
581 ret=-$E_GSU_BAD_COMMAND
582 result="$arg"
583 gsu_err_msg
584 _gsu_print_available_commands
585 exit 1
586 }