From 423ede182b650bbdc3845ebaa5921cb064fdf083 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Sun, 19 Oct 2014 21:36:58 -0700 Subject: [PATCH] utilities: Add bash command-line completion script. This patch adds bash command-line completion script for ovs-appctl, ovs-dpctl, ovs-ofctl and ovsdb-tool command. Right now, the script can do the following: - display available completion or complete on unfinished user input (long option, subcommand, and argument). - once the subcommand (e.g. ofproto/trace) has been given, the script will print the subcommand format. - the script can convert between keywords like 'bridge/port/interface/dp' and the available record in ovsdb. The limitations are: - only support small set of important keywords (dp, datapath, bridge, switch, port, interface, iface). - does not support parsing of nested options (e.g. ovsdb-tool create [db [schema]]). - does not support expansion on repeatitive argument (e.g. ovs-dpctl show [dp...]). - only support matching on long options, and only in the format (--option [arg], i.e. should not use --option=[arg]). To use the script, either copy it inside /etc/bash_completion.d/ or manually run it via . ovs-command-compgen.bash. Also, a unit testsuite is provided as ovs-command-compgen-test.bash. It is suggested that this test script be run only inside tutorial/sandbox directory. For more info please refer to utilities/ovs-command-compgen.INSTALL.md. Signed-off-by: Alex Wang Acked-by: Ben Pfaff --- NEWS | 3 + utilities/automake.mk | 3 + utilities/ovs-command-compgen-test.bash | 688 +++++++++++++++++++++++ utilities/ovs-command-compgen.INSTALL.md | 57 ++ utilities/ovs-command-compgen.bash | 614 ++++++++++++++++++++ 5 files changed, 1365 insertions(+) create mode 100755 utilities/ovs-command-compgen-test.bash create mode 100644 utilities/ovs-command-compgen.INSTALL.md create mode 100755 utilities/ovs-command-compgen.bash diff --git a/NEWS b/NEWS index 66e57bc7b..d27118823 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,8 @@ Post-v2.3.0 --------------------- + - Add bash command-line completion support for ovs-appctl/ovs-dpctl/ + ovs-ofctl/ovsdb-tool commands. Please check + utilities/ovs-command-compgen.INSTALL.md for how to use. - The "learn" action supports a new flag "delete_learned" that causes the learned flows to be deleted when the flow with the "learn" action is deleted. diff --git a/utilities/automake.mk b/utilities/automake.mk index 9ddbbe201..ccdd4dcfa 100644 --- a/utilities/automake.mk +++ b/utilities/automake.mk @@ -26,6 +26,9 @@ utilities/ovs-lib: $(top_builddir)/config.status EXTRA_DIST += \ utilities/ovs-check-dead-ifs.in \ + utilities/ovs-command-compgen.bash \ + utilities/ovs-command-compgen-test.bash \ + utilities/ovs-command-compgen.INSTALL.md \ utilities/ovs-ctl.in \ utilities/ovs-dev.py \ utilities/ovs-docker \ diff --git a/utilities/ovs-command-compgen-test.bash b/utilities/ovs-command-compgen-test.bash new file mode 100755 index 000000000..9de0d12a5 --- /dev/null +++ b/utilities/ovs-command-compgen-test.bash @@ -0,0 +1,688 @@ +#!/bin/bash +# +# Tests for the ovs-command-compgen.bash +# +# Please run this with ovs-command-compgen.bash script inside +# ovs-sandbox, under the same directory. +# +# For information about running the ovs-sandbox, please refer to +# the tutorial directory. +# +# +# +COMP_OUTPUT= +TMP= +EXPECT= +TEST_RESULT= + +TEST_COUNTER=0 +TEST_COMMANDS=(ovs-appctl ovs-ofctl ovs-dpctl ovsdb-tool) +TEST_APPCTL_TARGETS=(ovs-vswitchd ovsdb-server ovs-ofctl) + +# +# Helper functions. +# +get_command_format() { + local input="$@" + + echo "$(grep -A 1 "Command format" <<< "$input" | tail -n+2)" +} + +get_argument_expansion() { + local input="$@" + + echo "$(grep -- "available completions for keyword" <<< "$input" | sed -e 's/^[ \t]*//')" +} + +get_available_completions() { + local input="$@" + + echo "$(sed -e '1,/Available/d' <<< "$input" | tail -n+2)" +} + +generate_expect_completions() { + local keyword="$1" + local completions="$2" + + echo "available completions for keyword \"$keyword\": $completions" \ + | sed -e 's/[ \t]*$//' +} + +reset_globals() { + COMP_OUTPUT= + TMP= + EXPECT= + TEST_RESULT= +} + +# +# $1: Test name. +# $2: ok or fail. +# +print_result() { + (( TEST_COUNTER++ )) + printf "%2d: %-70s %s\n" "$TEST_COUNTER" "$1" "$2" +} + +# +# $1: test stage +# $2: actual +# $3: expect +# +print_error() { + local stage="$1" + local actual="$2" + local expect="$3" + + printf "failed at stage_%s:\n" "$stage" + printf "actual output: %s\n" "$actual" + printf "expect output: %s\n" "$expect" +} + +# +# Sub-tests. +# +ovs_apptcl_TAB() { + local target="$1" + local target_line= + local comp_output tmp expect + + if [ -n "$target" ]; then + target_line="--target $target" + fi + comp_output="$(bash ovs-command-compgen.bash debug ovs-appctl $target_line TAB 2>&1)" + tmp="$(get_available_completions "$comp_output")" + expect="$(ovs-appctl --option | sort | sed -n '/^--.*/p' | cut -d '=' -f1) +$(ovs-appctl $target_line list-commands | tail -n +2 | cut -c3- | cut -d ' ' -f1 | sort)" + if [ "$tmp" = "$expect" ]; then + echo "ok" + else + echo "fail" + fi +} + +# +# Test preparation. +# +ovs-vsctl add-br br0 +ovs-vsctl add-port br0 p1 + + +# +# Begin the test. +# +cat <&1)" + TMP="$(get_argument_expansion "$COMP_OUTPUT" | sed -e 's/[ \t]*$//')" + EXPECT="$(generate_expect_completions "-generate" "-generate") +$(generate_expect_completions "packet" "")" + if [ "$TMP" != "$EXPECT" ]; then + print_error "5" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # check the available completions. + TMP="$(get_available_completions "$COMP_OUTPUT" | tr '\n' ' ' | sed -e 's/[ \t]*$//')" + EXPECT="-generate" + if [ "$TMP" != "$EXPECT" ]; then + print_error "6" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # set packet to some random string, there should be no more completions. + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl ofproto/trace ovs-system "in_port(123),mac(),ip,tcp" "ABSJDFLSDJFOIWEQR" TAB 2>&1)" + TMP="$(sed -e '/./,$!d' <<< "$COMP_OUTPUT")" + EXPECT="Command format: +ofproto/trace {[dp_name] odp_flow | bridge br_flow} [-generate|packet]" + if [ "$TMP" != "$EXPECT" ]; then + print_error "7" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # set argument to 'br0', should go to the bridge path. + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl ofproto/trace br0 TAB 2>&1)" + TMP="$(get_argument_expansion "$COMP_OUTPUT" | sed -e 's/[ \t]*$//')" + EXPECT="$(generate_expect_completions "br_flow" "")" + if [ "$TMP" != "$EXPECT" ]; then + print_error "8" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # check the available completions. + TMP="$(get_available_completions "$COMP_OUTPUT" | tr '\n' ' ' | sed -e 's/[ \t]*$//')" + EXPECT= + if [ "$TMP" != "$EXPECT" ]; then + print_error "9" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # set argument to some random string, should go to the odp_flow path. + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl ofproto/trace "in_port(123),mac(),ip,tcp" TAB 2>&1)" + TMP="$(get_argument_expansion "$COMP_OUTPUT" | sed -e 's/[ \t]*$//')" + EXPECT="$(generate_expect_completions "-generate" "-generate") +$(generate_expect_completions "packet" "")" + if [ "$TMP" != "$EXPECT" ]; then + print_error "10" "$TMP" "$EXPEC"T + TEST_RESULT=fail + break + fi + + # check the available completions. + TMP="$(get_available_completions "$COMP_OUTPUT" | tr '\n' ' ' | sed -e 's/[ \t]*$//')" + EXPECT="-generate" + if [ "$TMP" != "$EXPECT" ]; then + print_error "11" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + TEST_RESULT=ok +done + +print_result "complex completion check - ofproto/trace" "$TEST_RESULT" + + +# complex completion check - vlog/set +# vlog/set {spec | PATTERN:facility:pattern} +# test non expandable arguments + +reset_globals + +for i in loop_once; do + # check the top level completion. + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl vlog/set TAB 2>&1)" + TMP="$(get_argument_expansion "$COMP_OUTPUT" | sed -e 's/[ \t]*$//')" + EXPECT="$(generate_expect_completions "PATTERN:facility:pattern" "") +$(generate_expect_completions "spec" "")" + if [ "$TMP" != "$EXPECT" ]; then + print_error "1" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # check the available completions. + TMP="$(get_available_completions "$COMP_OUTPUT" | tr '\n' ' ' | sed -e 's/[ \t]*$//')" + EXPECT= + if [ "$TMP" != "$EXPECT" ]; then + print_error "2" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # set argument to random 'abcd', there should be no more completions. + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl vlog/set abcd TAB 2>&1)" + TMP="$(sed -e '/./,$!d' <<< "$COMP_OUTPUT")" + EXPECT="Command format: +vlog/set {spec | PATTERN:facility:pattern}" + if [ "$TMP" != "$EXPECT" ]; then + print_error "3" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + TEST_RESULT=ok +done + +print_result "complex completion check - vlog/set" "$TEST_RESULT" + + +# complete after delete port + +reset_globals +ovs-vsctl del-port p1 + +for i in loop_once; do + # check match on interface, there should be no available interface expansion. + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl bfd/set-forwarding TAB 2>&1)" + TMP="$(get_argument_expansion "$COMP_OUTPUT" | sed -e 's/[ \t]*$//')" + EXPECT="$(generate_expect_completions "normal" "") +$(generate_expect_completions "false" "") +$(generate_expect_completions "true" "") +$(generate_expect_completions "interface" "")" + if [ "$TMP" != "$EXPECT" ]; then + print_error "1" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # check the available completions. + TMP="$(get_available_completions "$COMP_OUTPUT" | tr '\n' ' ' | sed -e 's/[ \t]*$//')" + EXPECT= + if [ "$TMP" != "$EXPECT" ]; then + print_error "2" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # check match on port, there should be no p1 as port. + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl lacp/show TAB 2>&1)" + TMP="$(get_argument_expansion "$COMP_OUTPUT" | sed -e 's/[ \t]*$//')" + EXPECT="$(generate_expect_completions "port" "br0")" + if [ "$TMP" != "$EXPECT" ]; then + print_error "3" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # check the available completions. + TMP="$(get_available_completions "$COMP_OUTPUT" | tr '\n' ' ' | sed -e 's/[ \t]*$//')" + EXPECT="br0" + if [ "$TMP" != "$EXPECT" ]; then + print_error "4" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + TEST_RESULT=ok +done + +print_result "complete after delete port" "$TEST_RESULT" + + +# complete after delete bridge + +reset_globals +ovs-vsctl del-br br0 +for i in loop_once; do + # check match on port, there should be no p1 as port. + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl bridge/dump-flows TAB 2>&1)" + TMP="$(get_argument_expansion "$COMP_OUTPUT" | sed -e 's/[ \t]*$//')" + EXPECT="$(generate_expect_completions "bridge" "")" + if [ "$TMP" != "$EXPECT" ]; then + print_error "1" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # check the available completions. + TMP="$(get_available_completions "$COMP_OUTPUT" | tr '\n' ' ' | sed -e 's/[ \t]*$//')" + EXPECT= + if [ "$TMP" != "$EXPECT" ]; then + print_error "2" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + # check 'ovs-ofctl monitor [misslen] [invalid_ttl] [watch:[...]]', should + # not show any available completion. + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-ofctl monitor non_exist_br TAB 2>&1)" + TMP="$(get_argument_expansion "$COMP_OUTPUT" | sed -e 's/[ \t]*$//')" + EXPECT= + if [ "$TMP" != "$EXPECT" ]; then + print_error "3" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + TEST_RESULT=ok +done + +print_result "complete after delete bridge" "$TEST_RESULT" + + +# negative test - incorrect subcommand + +reset_globals + +for i in loop_once; do + # incorrect subcommand + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl ERROR 2>&1)" + TMP="$(echo "$COMP_OUTPUT" | sed -e 's/[ \t]*$//' | sed -e '/./,$!d')" + EXPECT= + if [ "$TMP" != "$EXPECT" ]; then + print_error "1" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl ERROR TAB 2>&1)" + TMP="$(echo "$COMP_OUTPUT" | sed -e 's/[ \t]*$//' | sed -e '/./!d')" + EXPECT="Command format:" + if [ "$TMP" != "$EXPECT" ]; then + print_error "2" "$TMP" "$EXPECT" + TEST_RESULT=fail + break + fi + + TEST_RESULT=ok +done + +print_result "negative test - incorrect subcommand" "$TEST_RESULT" + + +# negative test - no ovs-vswitchd +# negative test - no ovsdb-server +# negative test - no ovs-ofctl +# should not see any error. + +reset_globals +killall ovs-vswitchd ovsdb-server + +for i in ${TEST_APPCTL_TARGETS[@]}; do + for j in loop_once; do + reset_globals + + daemon="$i" + + # should show no avaiable subcommands. + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl --target $daemon TAB 2>&1)" + TMP="$(get_available_completions "$COMP_OUTPUT")" + EXPECT="$(ovs-appctl --option | sort | sed -n '/^--.*/p' | cut -d '=' -f1)" + if [ "$TMP" != "$EXPECT" ]; then + TEST_RESULT=fail + break + fi + + # should not match any input. + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovs-appctl --target $daemon ERROR SUBCMD TAB 2>&1)" + TMP="$(echo "$COMP_OUTPUT" | sed -e 's/[ \t]*$//' | sed -e '/./!d')" + EXPECT="Command format:" + if [ "$TMP" != "$EXPECT" ]; then + TEST_RESULT=fail + break + fi + + TEST_RESULT=ok + done + print_result "negative test - no $daemon" "$TEST_RESULT" +done + + +# negative test - do not match on nested option + +reset_globals + +for i in loop_once; do + COMP_OUTPUT="$(bash ovs-command-compgen.bash debug ovsdb-tool create TAB 2>&1)" + TMP="$(get_available_completions "$COMP_OUTPUT")" + EXPECT= + if [ "$TMP" != "$EXPECT" ]; then + TEST_RESULT=fail + break + fi + + TEST_RESULT=ok +done + +print_result "negative test - do not match on nested option" "$TEST_RESULT" \ No newline at end of file diff --git a/utilities/ovs-command-compgen.INSTALL.md b/utilities/ovs-command-compgen.INSTALL.md new file mode 100644 index 000000000..e77ead54f --- /dev/null +++ b/utilities/ovs-command-compgen.INSTALL.md @@ -0,0 +1,57 @@ +Using bash command-line completion script +----------------------------------------- + +ovs-command-compgen.bash adds bash command-line completion support +for ovs-appctl, ovs-dpctl, ovs-ofctl and ovsdb-tool commands. + +Features: +--------- + + display available completion or complete on unfinished user input + (long option, subcommand, and argument). + + once the subcommand (e.g. ofproto/trace) has been given, the + script will print the subcommand format. + + the script can convert between keywords like 'bridge/port/interface/dp' + and the available record in ovsdb. + +Limitations: +------------ + + only support small set of important keywords + (dp, datapath, bridge, switch, port, interface, iface). + + does not support parsing of nested options + (e.g. ovsdb-tool create [db [schema]]). + + does not support expansion on repeatitive argument + (e.g. ovs-dpctl show [dp...]). + + only support matching on long options, and only in the format + (--option [arg], i.e. should not use --option=[arg]). + + +How to use: +----------- + + To use the script, either copy it inside /etc/bash_completion.d/ + or manually run it via . ovs-command-compgen.bash. + +Test: +----- + + An unit testsuite is provided as ovs-command-compgen-test.bash. + To run the test, first enter ovs sandbox via: + + make sandbox && cd sandbox + + Then copy both ovs-command-compgen-test.bash and ovs-command-compgen.bash + to the current directory. Finally, run the test via: + + bash ovs-command-compgen-test.bash + +Bug Reporting: +-------------- + +Please report problems to bugs@openvswitch.org. \ No newline at end of file diff --git a/utilities/ovs-command-compgen.bash b/utilities/ovs-command-compgen.bash new file mode 100755 index 000000000..c87445a67 --- /dev/null +++ b/utilities/ovs-command-compgen.bash @@ -0,0 +1,614 @@ +#!/bin/bash +# +# A bash command completion script for ovs-appctl. +# +# +# Right now, the script can do the following: +# +# - display available completion or complete on unfinished user input +# (long option, subcommand, and argument). +# +# - once the subcommand (e.g. ofproto/trace) has been given, the +# script will print the subcommand format. +# +# - the script can convert between keywords like 'bridge/port/interface/dp' +# and the available record in ovsdb. +# +# The limitation are: +# +# - only support small set of important keywords +# (dp, datapath, bridge, switch, port, interface, iface). +# +# - does not support parsing of nested option +# (e.g. ovsdb-tool create [db [schema]]). +# +# - does not support expansion on repeatitive argument +# (e.g. ovs-dpctl show [dp...]). +# +# - only support matching on long options, and only in the format +# (--option [arg], i.e. should not use --option=[arg]). +# +# +# +# Keywords +# ======== +# +# +# +# Expandable keywords. +_KWORDS=(bridge switch port interface iface dp_name dp) +# Command name. +_COMMAND= +# Printf enabler. +_PRINTF_ENABLE= +# Bash prompt. +_BASH_PROMPT= +# Output to the compgen. +_COMP_WORDLIST= + +# +# For ovs-appctl command only. +# +# Target in the current completion, default ovs-vswitchd. +_APPCTL_TARGET= +# Possible targets. +_POSSIBLE_TARGETS="ovs-vswitchd ovsdb-server ovs-ofctl" + +# Command Extraction +# ================== +# +# +# +# Extracts all subcommands of 'command'. +# If fails, returns nothing. +extract_subcmds() { + local command=$_COMMAND + local target= + local subcmds error + + if [ -n "$_APPCTL_TARGET" ]; then + target="--target $_APPCTL_TARGET" + fi + + subcmds="$($command $target list-commands 2>/dev/null | tail -n +2 | cut -c3- \ + | cut -d ' ' -f1)" || error="TRUE" + + if [ -z "$error" ]; then + echo "$subcmds" + fi +} + +# Extracts all long options of ovs-appctl. +# If fails, returns nothing. +extract_options() { + local command=$_COMMAND + local options error + + options="$($command --option 2>/dev/null | sort | sed -n '/^--.*/p' | cut -d '=' -f1)" \ + || error="TRUE" + + if [ -z "$error" ]; then + echo "$options" + fi +} + +# Returns the option format, if the option asks for an argument. +# If fails, returns nothing. +option_require_arg() { + local command=$_COMMAND + local option=$1 + local require_arg error + + require_arg="$($command --option | sort | sed -n '/^--.*/p' | grep -- "$option" | grep -- "=")" \ + || error="TRUE" + + if [ -z "$error" ]; then + echo "$require_arg" + fi +} + +# Combination Discovery +# ===================== +# +# +# +# Given the subcommand formats, finds all possible completions +# at current completion level. +find_possible_comps() { + local combs="$@" + local comps= + local line + + while read line; do + local arg= + + for arg in $line; do + # If it is an optional argument, gets all completions, + # and continues. + if [ -n "$(sed -n '/^\[.*\]$/p' <<< "$arg")" ]; then + local opt_arg="$(sed -e 's/^\[\(.*\)\]$/\1/' <<< "$arg")" + local opt_args=() + + IFS='|' read -a opt_args <<< "$opt_arg" + comps="${opt_args[@]} $comps" + # If it is in format "\[*", it is a start of nested + # option, do not parse. + elif [ -n "$(sed -n "/^\[.*$/p" <<< "$arg")" ]; then + break; + # If it is a compulsory argument, adds it to the comps + # and break, since all following args are for next stage. + else + local args=() + + IFS='|' read -a args <<< "$arg" + comps="${args[@]} $comps" + break; + fi + done + done <<< "$combs" + + echo "$comps" +} + +# Given the subcommand format, and the current command line input, +# finds all possible completions. +subcmd_find_comp_based_on_input() { + local format="$1" + local cmd_line=($2) + local mult= + local combs= + local comps= + local arg line + + # finds all combinations by searching for '{}'. + # there should only be one '{}', otherwise, the + # command format should be changed to multiple commands. + mult="$(sed -n 's/^.*{\(.*\)}.*$/ \1/p' <<< "$format" | tr '|' '\n' | cut -c1-)" + if [ -n "$mult" ]; then + while read line; do + local tmp= + + tmp="$(sed -e "s@{\(.*\)}@$line@" <<< "$format")" + combs="$combs@$tmp" + done <<< "$mult" + combs="$(tr '@' '\n' <<< "$combs")" + else + combs="$format" + fi + + # Now, starts from the first argument, narrows down the + # subcommand format combinations. + for arg in "${subcmd_line[@]}"; do + local kword possible_comps + + # Finds next level possible comps. + possible_comps=$(find_possible_comps "$combs") + # Finds the kword. + kword="$(arg_to_kwords "$arg" "$possible_comps")" + # Returns if could not find 'kword' + if [ -z "$kword" ]; then + return + fi + # Trims the 'combs', keeps context only after 'kword'. + if [ -n "$combs" ]; then + combs="$(sed -n "s@^.*\[\?$kword|\?[a-z_]*\]\? @@p" <<< "$combs")" + fi + done + comps="$(find_possible_comps "$combs")" + + echo "$(kwords_to_args "$comps")" +} + + + +# Helper +# ====== +# +# +# +# Prints the input to stderr. $_PRINTF_ENABLE must be filled. +printf_stderr() { + local stderr_out="$@" + + if [ -n "$_PRINTF_ENABLE" ]; then + printf "\n$stderr_out" 1>&2 + fi +} + +# Extracts the bash prompt PS1, outputs it with the input argument +# via 'printf_stderr'. +# +# Original idea inspired by: +# http://stackoverflow.com/questions/10060500/bash-how-to-evaluate-ps1-ps2 +# +# The code below is taken from Peter Amidon. His change makes it more +# robust. +extract_bash_prompt() { + local myPS1 v + + myPS1="$(sed 's/Begin prompt/\\Begin prompt/; s/End prompt/\\End prompt/' <<< "$PS1")" + v="$(bash --norc --noprofile -i 2>&1 <<< $'PS1=\"'"$myPS1"$'\" \n# Begin prompt\n# End prompt')" + v="${v##*# Begin prompt}" + _BASH_PROMPT="$(tail -n +2 <<< "${v%# End prompt*}" | sed 's/\\Begin prompt/Begin prompt/; s/\\End prompt/End prompt/')" +} + + + +# Keyword Conversion +# ================== +# +# +# +# All completion functions. +complete_bridge () { + local result error + + result=$(ovs-vsctl list-br 2>/dev/null | grep -- "^$1") || error="TRUE" + + if [ -z "$error" ]; then + echo "${result}" + fi +} + +complete_port () { + local ports result error + local all_ports + + all_ports=$(ovs-vsctl --format=table \ + --no-headings \ + --columns=name \ + list Port 2>/dev/null) || error="TRUE" + ports=$(printf "$all_ports" | sort | tr -d '"' | uniq -u) + result=$(grep -- "^$1" <<< "$ports") + + if [ -z "$error" ]; then + echo "${result}" + fi +} + +complete_iface () { + local bridge bridges result error + + bridges=$(ovs-vsctl list-br 2>/dev/null) || error="TRUE" + for bridge in $bridges; do + local ifaces + + ifaces=$(ovs-vsctl list-ifaces "${bridge}" 2>/dev/null) || error="TRUE" + result="${result} ${ifaces}" + done + + if [ -z "$error" ]; then + echo "${result}" + fi +} + +complete_dp () { + local dps result error + + dps=$(ovs-appctl dpctl/dump-dps 2>/dev/null | cut -d '@' -f2) || error="TRUE" + result=$(grep -- "^$1" <<< "$dps") + + if [ -z "$error" ]; then + echo "${result}" + fi +} + +# Converts the argument (e.g. bridge/port/interface/dp name) to +# the corresponding keywords. +# Returns empty string if could not map the arg to any keyword. +arg_to_kwords() { + local arg="$1" + local possible_kwords=($2) + local non_parsables=() + local match= + local kword + + for kword in ${possible_kwords[@]}; do + case "$kword" in + bridge|switch) + match="$(complete_bridge "$arg")" + ;; + port) + match="$(complete_port "$arg")" + ;; + interface|iface) + match="$(complete_iface "$arg")" + ;; + dp_name|dp) + match="$(complete_dp "$arg")" + ;; + *) + if [ "$arg" = "$kword" ]; then + match="$kword" + else + non_parsables+=("$kword") + continue + fi + ;; + esac + + if [ -n "$match" ]; then + echo "$kword" + return + fi + done + + # If there is only one non-parsable kword, + # just assumes the user input it. + if [ "${#non_parsables[@]}" -eq "1" ]; then + echo "$non_parsables" + return + fi +} + +# Expands the keywords to the corresponding instance names. +kwords_to_args() { + local possible_kwords=($@) + local args=() + local kword + + for kword in ${possible_kwords[@]}; do + local match= + + case "${kword}" in + bridge|switch) + match="$(complete_bridge "")" + ;; + port) + match="$(complete_port "")" + ;; + interface|iface) + match="$(complete_iface "")" + ;; + dp_name|dp) + match="$(complete_dp "")" + ;; + -*) + # Treats option as kword as well. + match="$kword" + ;; + *) + match= + ;; + esac + match=$(echo "$match" | tr '\n' ' ' | sed -e 's/^[ \t]*//') + args+=( $match ) + if [ -n "$_PRINTF_ENABLE" ]; then + local output_stderr= + + if [ -z "$printf_expand_once" ]; then + printf_expand_once="once" + printf -v output_stderr "\nArgument expansion:\n" + fi + printf -v output_stderr "$output_stderr available completions \ +for keyword \"%s\": %s " "$kword" "$match" + + printf_stderr "$output_stderr" + fi + done + + echo "${args[@]}" +} + + + + +# Parse and Compgen +# ================= +# +# +# +# This function takes the current command line arguments as input, +# finds the command format and returns the possible completions. +parse_and_compgen() { + local command=$_COMMAND + local subcmd_line=($@) + local subcmd=${subcmd_line[0]} + local target= + local subcmd_format= + local comp_wordlist= + + if [ -n "$_APPCTL_TARGET" ]; then + target="--target $_APPCTL_TARGET" + fi + + # Extracts the subcommand format. + subcmd_format="$($command $target list-commands 2>/dev/null | tail -n +2 | cut -c3- \ + | awk -v opt=$subcmd '$1 == opt {print $0}' | tr -s ' ' )" + + # Prints subcommand format. + printf_stderr "$(printf "\nCommand format:\n%s" "$subcmd_format")" + + # Finds the possible completions based on input argument. + comp_wordlist="$(subcmd_find_comp_based_on_input "$subcmd_format" \ + "${subcmd_line[@]}")" + + echo "$comp_wordlist" +} + + + +# Compgen Helper +# ============== +# +# +# +# Takes the current command line arguments and returns the possible +# completions. +# +# At the beginning, the options are checked and completed. For ovs-appctl +# completion, The function looks for the --target option which gives the +# target daemon name. If it is not provided, by default, 'ovs-vswitchd' +# is used. +# +# Then, tries to locate and complete the subcommand. If the subcommand +# is provided, the following arguments are passed to the 'parse_and_compgen' +# function to figure out the corresponding completion of the subcommand. +# +# Returns the completion arguments on success. +ovs_comp_helper() { + local cmd_line_so_far=($@) + local comp_wordlist _subcmd options i + local j=-1 + + # Parse the command-line args till we find the subcommand. + for i in "${!cmd_line_so_far[@]}"; do + # if $i is not greater than $j, it means the previous iteration + # skips not-visited args. so, do nothing and catch up. + if [ $i -le $j ]; then continue; fi + j=$i + if [[ "${cmd_line_so_far[i]}" =~ ^--* ]]; then + # If --target is found, locate the target daemon. + # Else, it is an option command, fill the comp_wordlist with + # all options. + if [ "$_COMMAND" = "ovs-appctl" ] \ + && [[ "${cmd_line_so_far[i]}" =~ ^--target$ ]]; then + _APPCTL_TARGET="ovs-vswitchd" + + if [ -n "${cmd_line_so_far[j+1]}" ]; then + local daemon + + for daemon in $_POSSIBLE_TARGETS; do + # Greps "$daemon" in argument, since the argument may + # be the path to the pid file. + if [ "$daemon" = "${cmd_line_so_far[j+1]}" ]; then + _APPCTL_TARGET="$daemon" + ((j++)) + break + fi + done + continue + else + comp_wordlist="$_POSSIBLE_TARGETS" + break + fi + else + options="$(extract_options $_COMMAND)" + # See if we could find the exact option. + if [ "${cmd_line_so_far[i]}" = "$(grep -- "${cmd_line_so_far[i]}" <<< "$options")" ]; then + # If an argument is required and next argument is non-empty, + # skip it. Else, return directly. + if [ -n "$(option_require_arg "${cmd_line_so_far[i]}")" ]; then + ((j++)) + if [ -z "${cmd_line_so_far[j]}" ]; then + printf_stderr "\nOption requires an arugment." + return + fi + fi + continue + # Else, need to keep completing on option. + else + comp_wordlist="$options" + break + fi + fi + fi + # Takes the first non-option argument as subcmd. + _subcmd="${cmd_line_so_far[i]}" + break + done + + if [ -z "$comp_wordlist" ]; then + # If the subcommand is not found, provides all subcmds and options. + if [ -z "$_subcmd" ]; then + comp_wordlist="$(extract_subcmds) $(extract_options)" + # Else parses the current arguments and finds the possible completions. + else + # $j stores the index of the subcmd in cmd_line_so_far. + comp_wordlist="$(parse_and_compgen "${cmd_line_so_far[@]:$j}")" + fi + fi + + echo "$comp_wordlist" +} + +# Compgen +# ======= +# +# +# +# The compgen function. +_ovs_command_complete() { + local cur prev + + _COMMAND=${COMP_WORDS} # element 0 is the command. + COMPREPLY=() + cur=${COMP_WORDS[COMP_CWORD]} + + # Do not print anything at first [TAB] execution. + if [ "$COMP_TYPE" -eq "9" ]; then + _PRINTF_ENABLE= + else + _PRINTF_ENABLE="enabled" + fi + + # Extracts bash prompt PS1. + if [ "$1" != "debug" ]; then + extract_bash_prompt + fi + + # Invokes the helper function to get all available completions. + # Always not input the 'COMP_WORD' at 'COMP_CWORD', since it is + # the one to be completed. + _COMP_WORDLIST="$(ovs_comp_helper \ + ${COMP_WORDS[@]:1:COMP_CWORD-1})" + + # This is a hack to prevent autocompleting when there is only one + # available completion and printf disabled. + if [ -z "$_PRINTF_ENABLE" ] && [ -n "$_COMP_WORDLIST" ]; then + _COMP_WORDLIST="$_COMP_WORDLIST none void no-op" + fi + + # Prints all available completions to stderr. If there is only one matched + # completion, do nothing. + if [ -n "$_PRINTF_ENABLE" ] \ + && [ -n "$(echo $_COMP_WORDLIST | tr ' ' '\n' | \ + grep -- "^$cur")" ]; then + printf_stderr "\nAvailable completions:\n" + fi + + # If there is no match between '$cur' and the '$_COMP_WORDLIST' + # prints a bash prompt since the 'complete' will not print it. + if [ -n "$_PRINTF_ENABLE" ] \ + && [ -z "$(echo $_COMP_WORDLIST | tr ' ' '\n' | grep -- "^$cur")" ] \ + && [ "$1" != "debug" ]; then + printf_stderr "\n$_BASH_PROMPT${COMP_WORDS[@]}" + fi + + if [ "$1" = "debug" ]; then + printf_stderr "$(echo $_COMP_WORDLIST | tr ' ' '\n' | sort -u | grep -- "$cur")\n" + else + COMPREPLY=( $(compgen -W "$(echo $_COMP_WORDLIST | tr ' ' '\n' \ + | sort -u)" -- $cur) ) + fi + + return 0 +} + +# Needed for the sorting of completions in display. +export LC_ALL=C + +# Debug mode. +if [ "$1" = "debug" ]; then + shift + COMP_TYPE=0 + COMP_WORDS=($@) + COMP_CWORD="$(expr $# - 1)" + + # If the last argument is TAB, it means that the previous + # argument is already complete and script should complete + # next argument which is not input yet. This hack is for + # compromising the fact that bash cannot take unquoted + # empty argument. + if [ "${COMP_WORDS[-1]}" = "TAB" ]; then + COMP_WORDS[${#COMP_WORDS[@]}-1]="" + fi + + _ovs_command_complete "debug" +# Normal compgen mode. +else + complete -F _ovs_command_complete ovs-appctl + complete -F _ovs_command_complete ovs-ofctl + complete -F _ovs_command_complete ovs-dpctl + complete -F _ovs_command_complete ovsdb-tool +fi \ No newline at end of file -- 2.20.1