utilities: Add bash command-line completion script.
[cascardo/ovs.git] / utilities / ovs-command-compgen.bash
1 #!/bin/bash
2 #
3 # A bash command completion script for ovs-appctl.
4 #
5 #
6 # Right now, the script can do the following:
7 #
8 #    - display available completion or complete on unfinished user input
9 #      (long option, subcommand, and argument).
10 #
11 #    - once the subcommand (e.g. ofproto/trace) has been given, the
12 #      script will print the subcommand format.
13 #
14 #    - the script can convert between keywords like 'bridge/port/interface/dp'
15 #      and the available record in ovsdb.
16 #
17 # The limitation are:
18 #
19 #    - only support small set of important keywords
20 #      (dp, datapath, bridge, switch, port, interface, iface).
21 #
22 #    - does not support parsing of nested option
23 #      (e.g. ovsdb-tool create [db [schema]]).
24 #
25 #    - does not support expansion on repeatitive argument
26 #      (e.g. ovs-dpctl show [dp...]).
27 #
28 #    - only support matching on long options, and only in the format
29 #      (--option [arg], i.e. should not use --option=[arg]).
30 #
31 #
32 #
33 # Keywords
34 # ========
35 #
36 #
37 #
38 # Expandable keywords.
39 _KWORDS=(bridge switch port interface iface dp_name dp)
40 # Command name.
41 _COMMAND=
42 # Printf enabler.
43 _PRINTF_ENABLE=
44 # Bash prompt.
45 _BASH_PROMPT=
46 # Output to the compgen.
47 _COMP_WORDLIST=
48
49 #
50 # For ovs-appctl command only.
51 #
52 # Target in the current completion, default ovs-vswitchd.
53 _APPCTL_TARGET=
54 # Possible targets.
55 _POSSIBLE_TARGETS="ovs-vswitchd ovsdb-server ovs-ofctl"
56
57 # Command Extraction
58 # ==================
59 #
60 #
61 #
62 # Extracts all subcommands of 'command'.
63 # If fails, returns nothing.
64 extract_subcmds() {
65     local command=$_COMMAND
66     local target=
67     local subcmds error
68
69     if [ -n "$_APPCTL_TARGET" ]; then
70         target="--target $_APPCTL_TARGET"
71     fi
72
73     subcmds="$($command $target list-commands 2>/dev/null | tail -n +2 | cut -c3- \
74                  | cut -d ' ' -f1)" || error="TRUE"
75
76     if [ -z "$error" ]; then
77         echo "$subcmds"
78     fi
79 }
80
81 # Extracts all long options of ovs-appctl.
82 # If fails, returns nothing.
83 extract_options() {
84     local command=$_COMMAND
85     local options error
86
87     options="$($command --option 2>/dev/null | sort | sed -n '/^--.*/p' | cut -d '=' -f1)" \
88         || error="TRUE"
89
90     if [ -z "$error" ]; then
91         echo "$options"
92     fi
93 }
94
95 # Returns the option format, if the option asks for an argument.
96 # If fails, returns nothing.
97 option_require_arg() {
98     local command=$_COMMAND
99     local option=$1
100     local require_arg error
101
102     require_arg="$($command --option | sort | sed -n '/^--.*/p' | grep -- "$option" | grep -- "=")" \
103         || error="TRUE"
104
105     if [ -z "$error" ]; then
106         echo "$require_arg"
107     fi
108 }
109
110 # Combination Discovery
111 # =====================
112 #
113 #
114 #
115 # Given the subcommand formats, finds all possible completions
116 # at current completion level.
117 find_possible_comps() {
118     local combs="$@"
119     local comps=
120     local line
121
122     while read line; do
123         local arg=
124
125         for arg in $line; do
126             # If it is an optional argument, gets all completions,
127             # and continues.
128             if [ -n "$(sed -n '/^\[.*\]$/p' <<< "$arg")" ]; then
129                 local opt_arg="$(sed -e 's/^\[\(.*\)\]$/\1/' <<< "$arg")"
130                 local opt_args=()
131
132                 IFS='|' read -a opt_args <<< "$opt_arg"
133                 comps="${opt_args[@]} $comps"
134             # If it is in format "\[*", it is a start of nested
135             # option, do not parse.
136             elif [ -n "$(sed -n "/^\[.*$/p" <<< "$arg")" ]; then
137                 break;
138             # If it is a compulsory argument, adds it to the comps
139             # and break, since all following args are for next stage.
140             else
141                 local args=()
142
143                 IFS='|' read -a args <<< "$arg"
144                 comps="${args[@]} $comps"
145                 break;
146             fi
147         done
148     done <<< "$combs"
149
150     echo "$comps"
151 }
152
153 # Given the subcommand format, and the current command line input,
154 # finds all possible completions.
155 subcmd_find_comp_based_on_input() {
156     local format="$1"
157     local cmd_line=($2)
158     local mult=
159     local combs=
160     local comps=
161     local arg line
162
163     # finds all combinations by searching for '{}'.
164     # there should only be one '{}', otherwise, the
165     # command format should be changed to multiple commands.
166     mult="$(sed -n 's/^.*{\(.*\)}.*$/ \1/p' <<< "$format" | tr '|' '\n' | cut -c1-)"
167     if [ -n "$mult" ]; then
168         while read line; do
169             local tmp=
170
171             tmp="$(sed -e "s@{\(.*\)}@$line@" <<< "$format")"
172             combs="$combs@$tmp"
173         done <<< "$mult"
174         combs="$(tr '@' '\n' <<< "$combs")"
175     else
176         combs="$format"
177     fi
178
179     # Now, starts from the first argument, narrows down the
180     # subcommand format combinations.
181     for arg in "${subcmd_line[@]}"; do
182         local kword possible_comps
183
184         # Finds next level possible comps.
185         possible_comps=$(find_possible_comps "$combs")
186         # Finds the kword.
187         kword="$(arg_to_kwords "$arg" "$possible_comps")"
188         # Returns if could not find 'kword'
189         if [ -z "$kword" ]; then
190             return
191         fi
192         # Trims the 'combs', keeps context only after 'kword'.
193         if [ -n "$combs" ]; then
194             combs="$(sed -n "s@^.*\[\?$kword|\?[a-z_]*\]\? @@p" <<< "$combs")"
195         fi
196     done
197     comps="$(find_possible_comps "$combs")"
198
199     echo "$(kwords_to_args "$comps")"
200 }
201
202
203
204 # Helper
205 # ======
206 #
207 #
208 #
209 # Prints the input to stderr.  $_PRINTF_ENABLE must be filled.
210 printf_stderr() {
211     local stderr_out="$@"
212
213     if [ -n "$_PRINTF_ENABLE" ]; then
214         printf "\n$stderr_out" 1>&2
215     fi
216 }
217
218 # Extracts the bash prompt PS1, outputs it with the input argument
219 # via 'printf_stderr'.
220 #
221 # Original idea inspired by:
222 # http://stackoverflow.com/questions/10060500/bash-how-to-evaluate-ps1-ps2
223 #
224 # The code below is taken from Peter Amidon.  His change makes it more
225 # robust.
226 extract_bash_prompt() {
227     local myPS1 v
228
229     myPS1="$(sed 's/Begin prompt/\\Begin prompt/; s/End prompt/\\End prompt/' <<< "$PS1")"
230     v="$(bash --norc --noprofile -i 2>&1 <<< $'PS1=\"'"$myPS1"$'\" \n# Begin prompt\n# End prompt')"
231     v="${v##*# Begin prompt}"
232     _BASH_PROMPT="$(tail -n +2 <<< "${v%# End prompt*}" | sed 's/\\Begin prompt/Begin prompt/; s/\\End prompt/End prompt/')"
233 }
234
235
236
237 # Keyword Conversion
238 # ==================
239 #
240 #
241 #
242 # All completion functions.
243 complete_bridge () {
244     local result error
245
246     result=$(ovs-vsctl list-br 2>/dev/null | grep -- "^$1") || error="TRUE"
247
248     if [ -z "$error" ]; then
249         echo  "${result}"
250     fi
251 }
252
253 complete_port () {
254     local ports result error
255     local all_ports
256
257     all_ports=$(ovs-vsctl --format=table \
258         --no-headings \
259         --columns=name \
260         list Port 2>/dev/null) || error="TRUE"
261     ports=$(printf "$all_ports" | sort | tr -d '"' | uniq -u)
262     result=$(grep -- "^$1" <<< "$ports")
263
264     if [ -z "$error" ]; then
265         echo  "${result}"
266     fi
267 }
268
269 complete_iface () {
270     local bridge bridges result error
271
272     bridges=$(ovs-vsctl list-br 2>/dev/null) || error="TRUE"
273     for bridge in $bridges; do
274         local ifaces
275
276         ifaces=$(ovs-vsctl list-ifaces "${bridge}" 2>/dev/null) || error="TRUE"
277         result="${result} ${ifaces}"
278     done
279
280     if [ -z "$error" ]; then
281         echo  "${result}"
282     fi
283 }
284
285 complete_dp () {
286     local dps result error
287
288     dps=$(ovs-appctl dpctl/dump-dps 2>/dev/null | cut -d '@' -f2) || error="TRUE"
289     result=$(grep -- "^$1" <<< "$dps")
290
291     if [ -z "$error" ]; then
292         echo  "${result}"
293     fi
294 }
295
296 # Converts the argument (e.g. bridge/port/interface/dp name) to
297 # the corresponding keywords.
298 # Returns empty string if could not map the arg to any keyword.
299 arg_to_kwords() {
300     local arg="$1"
301     local possible_kwords=($2)
302     local non_parsables=()
303     local match=
304     local kword
305
306     for kword in ${possible_kwords[@]}; do
307         case "$kword" in
308             bridge|switch)
309                 match="$(complete_bridge "$arg")"
310                 ;;
311             port)
312                 match="$(complete_port "$arg")"
313                 ;;
314             interface|iface)
315                 match="$(complete_iface "$arg")"
316                 ;;
317             dp_name|dp)
318                 match="$(complete_dp "$arg")"
319                 ;;
320             *)
321                 if [ "$arg" = "$kword" ]; then
322                     match="$kword"
323                 else
324                     non_parsables+=("$kword")
325                     continue
326                 fi
327                 ;;
328         esac
329
330         if [ -n "$match" ]; then
331             echo "$kword"
332             return
333         fi
334     done
335
336     # If there is only one non-parsable kword,
337     # just assumes the user input it.
338     if [ "${#non_parsables[@]}" -eq "1" ]; then
339         echo "$non_parsables"
340         return
341     fi
342 }
343
344 # Expands the keywords to the corresponding instance names.
345 kwords_to_args() {
346     local possible_kwords=($@)
347     local args=()
348     local kword
349
350     for kword in ${possible_kwords[@]}; do
351         local match=
352
353         case "${kword}" in
354             bridge|switch)
355                 match="$(complete_bridge "")"
356                 ;;
357             port)
358                 match="$(complete_port "")"
359                 ;;
360             interface|iface)
361                 match="$(complete_iface "")"
362                 ;;
363             dp_name|dp)
364                 match="$(complete_dp "")"
365                 ;;
366             -*)
367                 # Treats option as kword as well.
368                 match="$kword"
369                 ;;
370             *)
371                 match=
372                 ;;
373         esac
374         match=$(echo "$match" | tr '\n' ' ' | sed -e 's/^[ \t]*//')
375         args+=( $match )
376         if [ -n "$_PRINTF_ENABLE" ]; then
377             local output_stderr=
378
379             if [ -z "$printf_expand_once" ]; then
380                 printf_expand_once="once"
381                 printf -v output_stderr "\nArgument expansion:\n"
382             fi
383             printf -v output_stderr "$output_stderr     available completions \
384 for keyword \"%s\": %s " "$kword" "$match"
385
386             printf_stderr "$output_stderr"
387         fi
388     done
389
390     echo "${args[@]}"
391 }
392
393
394
395
396 # Parse and Compgen
397 # =================
398 #
399 #
400 #
401 # This function takes the current command line arguments as input,
402 # finds the command format and returns the possible completions.
403 parse_and_compgen() {
404     local command=$_COMMAND
405     local subcmd_line=($@)
406     local subcmd=${subcmd_line[0]}
407     local target=
408     local subcmd_format=
409     local comp_wordlist=
410
411     if [ -n "$_APPCTL_TARGET" ]; then
412         target="--target $_APPCTL_TARGET"
413     fi
414
415     # Extracts the subcommand format.
416     subcmd_format="$($command $target list-commands 2>/dev/null | tail -n +2 | cut -c3- \
417                      | awk -v opt=$subcmd '$1 == opt {print $0}' | tr -s ' ' )"
418
419     # Prints subcommand format.
420     printf_stderr "$(printf "\nCommand format:\n%s" "$subcmd_format")"
421
422     # Finds the possible completions based on input argument.
423     comp_wordlist="$(subcmd_find_comp_based_on_input "$subcmd_format" \
424                      "${subcmd_line[@]}")"
425
426     echo "$comp_wordlist"
427 }
428
429
430
431 # Compgen Helper
432 # ==============
433 #
434 #
435 #
436 # Takes the current command line arguments and returns the possible
437 # completions.
438 #
439 # At the beginning, the options are checked and completed.  For ovs-appctl
440 # completion, The function looks for the --target option which gives the
441 # target daemon name.  If it is not provided, by default, 'ovs-vswitchd'
442 # is used.
443 #
444 # Then, tries to locate and complete the subcommand.  If the subcommand
445 # is provided, the following arguments are passed to the 'parse_and_compgen'
446 # function to figure out the corresponding completion of the subcommand.
447 #
448 # Returns the completion arguments on success.
449 ovs_comp_helper() {
450     local cmd_line_so_far=($@)
451     local comp_wordlist _subcmd options i
452     local j=-1
453
454     # Parse the command-line args till we find the subcommand.
455     for i in "${!cmd_line_so_far[@]}"; do
456         # if $i is not greater than $j, it means the previous iteration
457         # skips not-visited args.  so, do nothing and catch up.
458         if [ $i -le $j ]; then continue; fi
459         j=$i
460         if [[ "${cmd_line_so_far[i]}" =~ ^--*  ]]; then
461             # If --target is found, locate the target daemon.
462             # Else, it is an option command, fill the comp_wordlist with
463             # all options.
464             if [ "$_COMMAND" = "ovs-appctl" ] \
465                 && [[ "${cmd_line_so_far[i]}" =~ ^--target$ ]]; then
466                 _APPCTL_TARGET="ovs-vswitchd"
467
468                 if [ -n "${cmd_line_so_far[j+1]}" ]; then
469                     local daemon
470
471                     for daemon in $_POSSIBLE_TARGETS; do
472                         # Greps "$daemon" in argument, since the argument may
473                         # be the path to the pid file.
474                         if [ "$daemon" = "${cmd_line_so_far[j+1]}" ]; then
475                             _APPCTL_TARGET="$daemon"
476                             ((j++))
477                             break
478                         fi
479                     done
480                     continue
481                 else
482                     comp_wordlist="$_POSSIBLE_TARGETS"
483                     break
484                 fi
485             else
486                 options="$(extract_options $_COMMAND)"
487                 # See if we could find the exact option.
488                 if [ "${cmd_line_so_far[i]}" = "$(grep -- "${cmd_line_so_far[i]}" <<< "$options")" ]; then
489                     # If an argument is required and next argument is non-empty,
490                     # skip it.  Else, return directly.
491                     if [ -n "$(option_require_arg "${cmd_line_so_far[i]}")" ]; then
492                         ((j++))
493                         if [ -z "${cmd_line_so_far[j]}" ]; then
494                             printf_stderr "\nOption requires an arugment."
495                             return
496                         fi
497                     fi
498                     continue
499                 # Else, need to keep completing on option.
500                 else
501                     comp_wordlist="$options"
502                     break
503                 fi
504             fi
505         fi
506         # Takes the first non-option argument as subcmd.
507         _subcmd="${cmd_line_so_far[i]}"
508         break
509     done
510
511     if [ -z "$comp_wordlist" ]; then
512         # If the subcommand is not found, provides all subcmds and options.
513         if [ -z "$_subcmd" ]; then
514             comp_wordlist="$(extract_subcmds) $(extract_options)"
515         # Else parses the current arguments and finds the possible completions.
516         else
517             # $j stores the index of the subcmd in cmd_line_so_far.
518             comp_wordlist="$(parse_and_compgen "${cmd_line_so_far[@]:$j}")"
519         fi
520     fi
521
522     echo "$comp_wordlist"
523 }
524
525 # Compgen
526 # =======
527 #
528 #
529 #
530 # The compgen function.
531 _ovs_command_complete() {
532   local cur prev
533
534   _COMMAND=${COMP_WORDS} # element 0 is the command.
535   COMPREPLY=()
536   cur=${COMP_WORDS[COMP_CWORD]}
537
538   # Do not print anything at first [TAB] execution.
539   if [ "$COMP_TYPE" -eq "9" ]; then
540       _PRINTF_ENABLE=
541   else
542       _PRINTF_ENABLE="enabled"
543   fi
544
545   # Extracts bash prompt PS1.
546   if [ "$1" != "debug" ]; then
547       extract_bash_prompt
548   fi
549
550   # Invokes the helper function to get all available completions.
551   # Always not input the 'COMP_WORD' at 'COMP_CWORD', since it is
552   # the one to be completed.
553   _COMP_WORDLIST="$(ovs_comp_helper \
554       ${COMP_WORDS[@]:1:COMP_CWORD-1})"
555
556   # This is a hack to prevent autocompleting when there is only one
557   # available completion and printf disabled.
558   if [ -z "$_PRINTF_ENABLE" ] && [ -n "$_COMP_WORDLIST" ]; then
559       _COMP_WORDLIST="$_COMP_WORDLIST none void no-op"
560   fi
561
562   # Prints all available completions to stderr.  If there is only one matched
563   # completion, do nothing.
564   if [ -n "$_PRINTF_ENABLE" ] \
565       && [ -n "$(echo $_COMP_WORDLIST | tr ' ' '\n' | \
566                 grep -- "^$cur")" ]; then
567       printf_stderr "\nAvailable completions:\n"
568   fi
569
570   # If there is no match between '$cur' and the '$_COMP_WORDLIST'
571   # prints a bash prompt since the 'complete' will not print it.
572   if [ -n "$_PRINTF_ENABLE" ] \
573       && [ -z "$(echo $_COMP_WORDLIST | tr ' ' '\n' | grep -- "^$cur")" ] \
574       && [ "$1" != "debug" ]; then
575       printf_stderr "\n$_BASH_PROMPT${COMP_WORDS[@]}"
576   fi
577
578   if [ "$1" = "debug" ]; then
579       printf_stderr "$(echo $_COMP_WORDLIST | tr ' ' '\n' | sort -u | grep -- "$cur")\n"
580   else
581       COMPREPLY=( $(compgen -W "$(echo $_COMP_WORDLIST | tr ' ' '\n' \
582                                  | sort -u)" -- $cur) )
583   fi
584
585   return 0
586 }
587
588 # Needed for the sorting of completions in display.
589 export LC_ALL=C
590
591 # Debug mode.
592 if [ "$1" = "debug" ]; then
593     shift
594     COMP_TYPE=0
595     COMP_WORDS=($@)
596     COMP_CWORD="$(expr $# - 1)"
597
598     # If the last argument is TAB, it means that the previous
599     # argument is already complete and script should complete
600     # next argument which is not input yet.  This hack is for
601     # compromising the fact that bash cannot take unquoted
602     # empty argument.
603     if [ "${COMP_WORDS[-1]}" = "TAB" ]; then
604         COMP_WORDS[${#COMP_WORDS[@]}-1]=""
605     fi
606
607     _ovs_command_complete "debug"
608 # Normal compgen mode.
609 else
610     complete -F _ovs_command_complete ovs-appctl
611     complete -F _ovs_command_complete ovs-ofctl
612     complete -F _ovs_command_complete ovs-dpctl
613     complete -F _ovs_command_complete ovsdb-tool
614 fi