diff --git a/Makefile.am b/Makefile.am
index 5ec9cf772b223ef858f84f7c296dd7cb045f351f..7ae24ef8830e1e58599fcda53b1435e347069853 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -934,6 +934,7 @@ dist_TESTS = \
 	test/ganeti-cleaner_unittest.bash \
 	test/import-export_unittest.bash \
 	test/cli-test.bash \
+	test/bash_completion.bash \
 	$(python_tests)
 
 nodist_TESTS =
@@ -986,6 +987,7 @@ srclink_files = \
 	test/ganeti-cleaner_unittest.bash \
 	test/import-export_unittest.bash \
 	test/cli-test.bash \
+	test/bash_completion.bash \
 	htest/offline-test.sh \
 	htest/cli-tests-defs.sh \
 	$(all_python_code) \
@@ -1030,6 +1032,8 @@ test/daemon-util_unittest.bash: daemons/daemon-util
 
 test/ganeti-cleaner_unittest.bash: daemons/ganeti-cleaner
 
+test/bash_completion.bash: doc/examples/bash_completion-debug
+
 tools/kvm-ifup: tools/kvm-ifup.in $(REPLACE_VARS_SED)
 	sed -f $(REPLACE_VARS_SED) < $< > $@
 	chmod +x $@
diff --git a/test/bash_completion.bash b/test/bash_completion.bash
new file mode 100755
index 0000000000000000000000000000000000000000..012ec27968339a9e479e5a7701e438e82b8b6f48
--- /dev/null
+++ b/test/bash_completion.bash
@@ -0,0 +1,235 @@
+#!/bin/bash
+#
+
+# Copyright (C) 2012 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+set -e -u -o pipefail
+
+# Disable any locales
+export LC_ALL=C
+
+readonly bash_completion=${TOP_BUILDDIR:-.}/doc/examples/bash_completion-debug
+readonly default_wordbreaks=$' \t\n"'\''@><=;|&(:'
+
+err() {
+  echo "$@"
+  echo 'Aborting'
+  exit 1
+}
+
+contains() {
+  local -r needle="$1"; shift
+
+  for value; do
+    if [[ "$value" = "$needle" ]]; then
+      return 0
+    fi
+  done
+
+  return 1
+}
+
+# Prepares a subshell for testing bash completion functions
+setup() {
+  local -r unused=UNUSED
+
+  set +e +u -o pipefail
+
+  . $bash_completion
+
+  COMP_KEY=$unused
+  COMP_LINE=$unused
+  COMP_POINT=$unused
+  COMP_TYPE=$unused
+  COMP_WORDBREAKS="$default_wordbreaks"
+
+  GANETI_COMPL_LOG=
+
+  unset COMP_CWORD
+  unset COMP_WORDS
+}
+
+# Check if default wordbreaks are still valid (this detects cases where Bash
+# were to change its built-in default value)
+# TODO: May need an update for older Bash versions; some didn't include the
+# colon character (':') in COMP_WORDBREAKS
+(
+  bashdef=$(env - bash --noprofile --norc -c 'echo -n "$COMP_WORDBREAKS"')
+  case "$bashdef" in
+    $default_wordbreaks) ;;
+    *)
+      err 'Bash uses unknown value for COMP_WORDBREAKS'
+      ;;
+  esac
+)
+
+# Check for --help
+for cmd in gnt-{instance,node,group,job}; do
+  (
+    setup
+    COMP_CWORD=2
+    COMP_WORDS=( $cmd list - )
+    _${cmd/-/_}
+    contains --help "${COMPREPLY[@]}" || \
+      err "'$cmd list' did not list --help as an option"
+  )
+done
+
+# Completing a yes/no option
+(
+  setup
+  COMP_CWORD=3
+  COMP_WORDS=( gnt-node modify --drained )
+  _gnt_node
+  if [[ "${COMPREPLY[*]}" != 'no yes' ]]; then
+    err "Completing '${COMP_WORDS[@]}' did not give correct result"
+  fi
+)
+
+# Completing a multiple-choice option
+(
+  setup
+  COMP_CWORD=2
+  COMP_WORDS=( gnt-debug allocator --disk-template=sh foo )
+  _gnt_debug
+  if [[ "${COMPREPLY[*]}" != sharedfile ]]; then
+    err "Completing '${COMP_WORDS[*]}' did not give correct result"
+  fi
+)
+
+# Completing a node name
+(
+  setup
+
+  # Override built-in function
+  _ganeti_nodes() {
+    echo aanode1 bbnode2 aanode3
+  }
+
+  COMP_CWORD=4
+  COMP_WORDS=( gnt-node modify --drained yes aa )
+  _gnt_node
+  if [[ "${COMPREPLY[*]}" != 'aanode1 aanode3' ]]; then
+    err 'Completing node names failed'
+  fi
+)
+
+# Completing an option when it's not at the end
+(
+  setup
+
+  # Override built-in function
+  _ganeti_instances() {
+    echo inst{1..5}
+  }
+
+  # Completing word in the middle
+  COMP_CWORD=2
+  COMP_WORDS=( gnt-instance list --o inst3 inst inst5 )
+  _gnt_node
+  contains --output "${COMPREPLY[@]}" || err 'Did not complete parameter'
+)
+
+# Completing an instance name
+(
+  setup
+
+  # Override built-in function
+  _ganeti_instances() {
+    echo inst{1..5}
+  }
+
+  # Completing word in the middle
+  COMP_CWORD=5
+  COMP_WORDS=( gnt-instance list -o foobar inst1 inst inst5 )
+  _gnt_instance
+  if [[ "${COMPREPLY[*]}" != "$(echo inst{1..5})" ]]; then
+    err "Completing '${COMP_WORDS[*]}' did not give correct result"
+  fi
+)
+
+# Need to test node expansion with different wordbreak settings
+[[ "$default_wordbreaks" == *:* ]] || \
+  err 'No colon in default wordbreak characters'
+for wb in "$default_wordbreaks" "${default_wordbreaks/:/}"; do
+  (
+    setup
+
+    # Override built-in function
+    _ganeti_nodes() {
+      echo node{A..C}
+    }
+
+    COMP_WORDBREAKS="$wb"
+
+    # Completing nodes
+    COMP_CWORD=3
+    COMP_WORDS=( gnt-instance add -n )
+    _gnt_instance
+    if [[ "${COMPREPLY[*]}" != 'nodeA nodeA: nodeB nodeB: nodeC nodeC:' ]]; then
+      err 'Got wrong node list'
+    fi
+
+    COMP_CWORD=3
+    COMP_WORDS=( gnt-instance add -n nodeB )
+    _gnt_instance
+    if [[ "${COMPREPLY[*]}" != 'nodeB nodeB:' ]]; then
+      err 'Got wrong node list'
+    fi
+
+    COMP_CWORD=3
+    COMP_WORDS=( gnt-instance add -n nodeC: )
+    _gnt_instance
+    if [[ "$COMP_WORDBREAKS" == *:* ]]; then
+      expected='nodeA nodeB'
+    else
+      expected='nodeC:nodeA nodeC:nodeB'
+    fi
+    if [[ "${COMPREPLY[*]}" != "$expected" ]]; then
+      err 'Got wrong node list'
+    fi
+  )
+done
+
+# Need to test different settings for the extglob shell option
+for opt in -u -s; do
+  verify_extglob() {
+    if [[ "$(shopt -p extglob)" != "shopt $opt extglob" ]]; then
+      err 'The "extglob" shell option has an unexpected value'
+    fi
+  }
+
+  (
+    shopt $opt extglob
+
+    verify_extglob
+    setup
+    verify_extglob
+
+    # Completing nodes
+    COMP_CWORD=4
+    COMP_WORDS=( gnt-instance add --os-type busybox --no-n )
+    _gnt_instance
+    if [[ "${COMPREPLY[*]}" != '--no-name-check --no-nics' ]]; then
+      err "Completing '${COMP_WORDS[*]}' did not give correct result"
+    fi
+    verify_extglob
+  )
+done
+
+exit 0