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