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