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