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