f5f4745a8856920a0203b02039eadce46f4bf2ef
[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
15 # of the subcommand is the first subexpression.
16 export gsu_command_regex='^com_\([-a-zA-Z_0-9]\+\)()'
17
18 _gsu_available_commands()
19 {
20 result="$({
21 printf "help\nman\nprefs\ncomplete\n"
22 sed -ne "s/$gsu_command_regex/\1/g;T;p" $0
23 } | sort | tr '\n' ' ')"
24 }
25
26 _gsu_print_available_commands()
27 {(
28 local i count
29 gsu_short_msg "Available commands:"
30 for i in $gsu_cmds; do
31 printf "$i"
32 count=$(($count + 1))
33 if test $(($count % 4)) -eq 0; then
34 echo
35 else
36 printf "\t"
37 if test ${#i} -lt 8; then
38 printf "\t"
39 fi
40 fi
41 done
42 echo
43 ) 2>&1
44 }
45
46 gsu_complete_options()
47 {
48 local opts="$1" cword="$2" cur
49 local -a words
50
51 shift 2
52 words=("$@")
53 cur="${words[$cword]}"
54 ret=0
55 [[ ! "$cur" == -* ]] && return
56
57 ret=0
58 for ((i=0; i < ${#opts}; i++)); do
59 opt="${opts:$i:1}"
60 [[ "$opt" == ":" ]] && continue
61 printf "%s" "-$opt "
62 let ret++
63 done
64 }
65
66 export gsu_prefs_txt="
67 Print the current preferences.
68
69 Usage: prefs [-e]
70
71 If -e is given, the config file is opened with the default editor. Without
72 options, the command prints out a list of all cmt config variables, together
73 with their current value and the default value."
74 _com_prefs()
75 {
76 local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}"
77
78 if [[ "$1" = "-e" ]]; then
79 ret=-$E_GSU_MKDIR
80 result="${conf%/*}"
81 mkdir -p "$result"
82 [[ $? -ne 0 ]] && return
83 ret=-$E_GSU_EDITOR
84 result="${EDITOR:-vi}"
85 "$result" "$conf"
86 [[ $? -ne 0 ]] && return
87 ret=$GSU_SUCCESS
88 return
89 fi
90
91 for ((i=0; i < ${#gsu_options[@]}; i++)); do
92 local name= option_type= default_value= required=
93 local description= help_text=
94 eval "${gsu_options[$i]}"
95 eval val='"$'${gsu_config_var_prefix}_$name'"'
96 case "$required" in
97 true|yes)
98 printf "# required"
99 ;;
100 *)
101 printf "# optional"
102 ;;
103 esac
104 printf " $option_type: $description"
105 if [[ "$required" != "yes" && "$required" != "true" ]]; then
106 printf " [$default_value]"
107 fi
108 echo
109 [[ -n "$help_text" ]] && sed -e '/^[ ]*$/d; s/^[ ]*/# /g' <<< "$help_text"
110 printf "$name=$val"
111 [[ "$val" == "$default_value" ]] && printf " # default"
112 echo
113 done
114 }
115
116 complete_prefs()
117 {
118 gsu_complete_options "e" "$@"
119 }
120
121 export gsu_man_txt="
122 Print the manual.
123
124 Usage: man"
125
126 _com_man()
127 {
128 local equal_signs="=================================================="
129 local minus_signs="--------------------------------------------------"
130 local com num
131
132 echo "$_gsu_self (_${gsu_banner_txt}_) manual"
133 echo "${equal_signs:0:${#_gsu_self} + ${#gsu_banner_txt} + 16}"
134 echo
135
136 sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
137 echo "----"
138 echo
139 echo "$_gsu_self usage"
140 echo "${minus_signs:0:${#_gsu_self} + 6}"
141 printf "\t"
142 _gsu_usage 2>&1
143 echo "Each command has its own set of options as described below."
144 echo
145 echo "----"
146 echo
147 echo "Available commands:"
148
149 _gsu_available_commands
150 for com in $result; do
151 num=${#com}
152 if test $num -lt 4; then
153 num=4
154 fi
155 echo "${minus_signs:0:$num}"
156 echo "$com"
157 echo "${minus_signs:0:$num}"
158 $0 help $com
159 echo
160 done
161 ret=$GSU_SUCCESS
162 }
163
164 _gsu_banner_msg()
165 {
166 local txt="### $_gsu_self --"
167 if test -z "$gsu_banner_txt"; then
168 txt="$txt set \$gsu_banner_txt to customize this message"
169 else
170 txt="$txt $gsu_banner_txt"
171 fi
172 gsu_short_msg "$txt ###"
173 }
174
175 export gsu_help_txt="
176 Print online help.
177
178 Usage: help [command]
179
180 Without arguments, print the list of available commands. Otherwise,
181 print the help text for the given command."
182
183 export gsu_complete_txt="
184 Command line completion.
185
186 Usage: complete [<cword> <word>...]
187
188 In the first form, the command prints all possible completions to stdout.
189 This can be used from the completion function of the shell.
190
191 Completion code suitable to be evaled is written to stdout if no argument
192 was given.
193 "
194
195 _com_help()
196 {
197 local a b
198 if test -z "$1"; then
199 _gsu_banner_msg 2>&1
200 _gsu_usage 2>&1
201 {
202 printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
203 printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
204 printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
205 printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
206 grep -A 2 "$gsu_command_regex" $0
207 } | grep -v -- '--' \
208 | sed -e "/$gsu_command_regex/bs" \
209 -e 'H;$!d;x;s/\n//g;b' \
210 -e :s \
211 -e 'x;s/\n//g;${p;x;}' \
212 | sed -e "s/${gsu_command_regex}#*/\1\t/" \
213 | sort \
214 | while read a b; do
215 printf "$a\t"
216 if test ${#a} -lt 8; then
217 printf "\t"
218 fi
219 echo "$b"
220 done
221 echo
222 echo "# Try $_gsu_self help <command> for info on <command>."
223 ret=$GSU_SUCCESS
224 return
225 fi
226 if test "$1" = "help"; then
227 echo "$gsu_help_txt"
228 ret=$GSU_SUCCESS
229 return
230 fi
231 if test "$1" = "man"; then
232 echo "$gsu_man_txt"
233 ret=$GSU_SUCCESS
234 return
235 fi
236 if test "$1" = "prefs"; then
237 echo "$gsu_prefs_txt"
238 ret=$GSU_SUCCESS
239 return
240 fi
241 if test "$1" = "complete"; then
242 echo "$gsu_complete_txt"
243 ret=$GSU_SUCCESS
244 return
245 fi
246 ret=$GSU_SUCCESS
247 if grep -q "^com_$1()" $0; then
248 sed -e "1,/^com_$1()$/d" -e '/^{/,$d' -e 's/^## *//' $0
249 return
250 fi
251 _gsu_print_available_commands
252 result="$1"
253 ret=-$E_GSU_BAD_COMMAND
254 }
255
256 complete_help()
257 {
258 _gsu_available_commands
259 echo "$result"
260 }
261
262 # Wrapper for bash's getopts.
263 #
264 # Aborts on programming errors such as missing or invalid option string. On
265 # success $result contains shell code that can be eval'ed. For each defined
266 # option x, the local variable o_x will be created when calling eval "$result".
267 # o_x contains true/false for options without an argument or the emtpy string/the
268 # given argument, depending on whether this option was contained in the "$@"
269 # array.
270 #
271 # Example:
272 # gsu_getopts abc:x:y
273 # eval "$result"
274 # [[ $ret -lt 0 ]] && return
275 #
276 # [[ "$o_a" = "true ]] && echo "The -a flag was given"
277 # [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
278 gsu_getopts()
279 {
280 local i c tab=' ' cr='
281 '
282
283 gsu_check_arg_count $# 1 1
284 if [[ $ret -lt 0 ]]; then
285 gsu_err_msg
286 exit 1
287 fi
288
289 ret=-$E_GSU_GETOPTS
290 result="invalid optstring $1"
291 if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
292 gsu_err_msg
293 exit 1
294 fi
295
296 for ((i=0; i < ${#1}; i++)); do
297 c=${1:$i:1}
298 case "$c" in
299 [a-zA-Z:]);;
300 *)
301 ret=-$E_GSU_GETOPTS
302 result="invalid character $c in optstring"
303 gsu_err_msg
304 exit 1
305 esac
306 done
307 result="local opt"
308 for ((i=0; i < ${#1}; i++)); do
309 c1=${1:$i:1}
310 c2=${1:$(($i + 1)):1}
311 result+=" o_$c1"
312 if [[ "$c2" = ":" ]]; then
313 let i++
314 else
315 result+="=false"
316 fi
317 done
318 result+="
319 OPTIND=1
320 while getopts $1 opt \"\$@\"; do
321 case \"\$opt\" in
322 "
323 for ((i=0; i < ${#1}; i++)); do
324 c1=${1:$i:1}
325 c2=${1:$(($i + 1)):1}
326 result+="$tab$tab$c1) o_$c1="
327 if [[ "$c2" = ":" ]]; then
328 result+="\"\$OPTARG\""
329 let i++
330 else
331 result+="true"
332 fi
333 result+=";;$cr"
334 done
335 result+="
336 *)
337 ret=-\$E_GSU_GETOPTS
338 result=\"invalid option given\"
339 return
340 ;;
341 esac
342 done
343 shift \$((\$OPTIND - 1))
344 "
345 ret=$GSU_SUCCESS
346 }
347
348 _com_complete()
349 {
350 local cmd n cword="$1"
351 local -a words
352
353 if (($# == 0)); then
354 cat <<EOF
355 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
356 local -a candidates;
357
358 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
359 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
360 EOF
361 fi
362
363 [[ -z "$cword" ]] && return
364 if (($cword <= 1)); then
365 _gsu_available_commands
366 echo "${result}"
367 ret=$GSU_SUCCESS
368 return
369 fi
370 shift
371 words=("$@")
372 cmd="${words[1]}"
373 ret=$GSU_SUCCESS # It's not an error if no completer was defined
374 [[ "$(type -t complete_$cmd)" != "function" ]] && return
375 complete_$cmd "$cword" "${words[@]}"
376 # ignore errors, they would only clutter the completion output
377 ret=$GSU_SUCCESS
378 }
379
380 gsu_cword_is_option_parameter()
381 {
382 local opts="$1" cword="$2" prev i n
383 local -a words
384
385 result=
386 (($cword == 0)) && return
387 ((${#opts} < 2)) && return
388
389 shift 2
390 words=("$@")
391 prev="${words[$(($cword - 1))]}"
392 [[ ! "$prev" == -* ]] && return
393
394 n=$((${#opts} - 1))
395 for ((i=0; i < $n; i++)); do
396 opt="${opts:$i:1}"
397 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
398 let i++
399 [[ "$prev" != "-$opt" ]] && continue
400 result="$opt"
401 return
402 done
403 ret=0
404 }
405
406 gsu()
407 {
408 local i
409 _gsu_setup
410 _gsu_available_commands
411 gsu_cmds="$result"
412 if test $# -eq 0; then
413 _gsu_usage
414 _gsu_print_available_commands
415 exit 1
416 fi
417 arg="$1"
418 shift
419 # check internal commands
420 if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then
421 _com_$arg "$@"
422 if [[ "$ret" -lt 0 ]]; then
423 gsu_err_msg
424 exit 1
425 fi
426 exit 0
427 fi
428
429 # external commands
430 for i in $gsu_cmds; do
431 if test "$arg" = "$i"; then
432 com_$arg "$@"
433 if [[ "$ret" -lt 0 ]]; then
434 gsu_err_msg
435 exit 1
436 fi
437 exit 0
438 fi
439 done
440
441 ret=-$E_GSU_BAD_COMMAND
442 result="$arg"
443 gsu_err_msg
444 _gsu_print_available_commands
445 exit 1
446 }
447
448 # Check number of arguments.
449 #
450 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
451 #
452 # Check that <num_given> is between <num1> and <num2> inclusively.
453 # If only <num1> ist given, num2 is assumed to be infinity.
454 #
455 # Examples:
456 # 0 0 no argument allowed
457 # 1 1 exactly one argument required
458 # 0 2 at most two arguments admissible
459 # 2 at least two arguments reqired
460 #
461 gsu_check_arg_count()
462 {
463 ret=-$E_GSU_BAD_ARG_COUNT
464 if [[ $# -eq 2 ]]; then # only num1 is given
465 result="at least $2 args required, $1 given"
466 [[ $1 -lt $2 ]] && return
467 ret=$GSU_SUCCESS
468 return
469 fi
470 # num1 and num2 given
471 result="need at least $2 args, $1 given"
472 [[ $1 -lt $2 ]] && return
473 result="need at most $3 args, $1 given"
474 [[ $1 -gt $3 ]] && return
475 ret=$GSU_SUCCESS
476 }