diff --git a/.codecov.yml b/.codecov.yml
index e45c500b76f35f90b88b7f3772d9c06a46e4ba0a..a70b19c39c1c6f9faaf6016b241dccc985486423 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -12,6 +12,5 @@ ignore:
   - lib/spack/docs/.*
   - lib/spack/external/.*
   - share/spack/qa/.*
-  - share/spack/spack-completion.bash
 
 comment: off
diff --git a/.coveragerc b/.coveragerc
index c0c5f76688b5b5f51c1372e10bdb48967a815070..7292badff531694292603a966141852bd9789c8c 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -9,6 +9,7 @@ omit =
      lib/spack/spack/test/*
      lib/spack/docs/*
      lib/spack/external/*
+     share/spack/qa/*
 
 [report]
 # Regexes for lines to exclude from consideration
diff --git a/lib/spack/llnl/util/argparsewriter.py b/lib/spack/llnl/util/argparsewriter.py
index ec6ea30df951c56d75aa5e84fb957512d742aa0a..f43595145e4493aca421cc3aeb155ae84be64dc8 100644
--- a/lib/spack/llnl/util/argparsewriter.py
+++ b/lib/spack/llnl/util/argparsewriter.py
@@ -4,201 +4,376 @@
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
 from __future__ import print_function
+
 import re
 import argparse
 import errno
 import sys
 
-
+from six import StringIO
+
+
+class Command(object):
+    """Parsed representation of a command from argparse.
+
+    This is a single command from an argparse parser. ``ArgparseWriter``
+    creates these and returns them from ``parse()``, and it passes one of
+    these to each call to ``format()`` so that we can take an action for
+    a single command.
+
+    Parts of a Command:
+      - prog: command name (str)
+      - description: command description (str)
+      - usage: command usage (str)
+      - positionals: list of positional arguments (list)
+      - optionals: list of optional arguments (list)
+      - subcommands: list of subcommand parsers (list)
+    """
+    def __init__(self, prog, description, usage,
+                 positionals, optionals, subcommands):
+        self.prog = prog
+        self.description = description
+        self.usage = usage
+        self.positionals = positionals
+        self.optionals = optionals
+        self.subcommands = subcommands
+
+
+# NOTE: The only reason we subclass argparse.HelpFormatter is to get access
+# to self._expand_help(), ArgparseWriter is not intended to be used as a
+# formatter_class.
 class ArgparseWriter(argparse.HelpFormatter):
     """Analyzes an argparse ArgumentParser for easy generation of help."""
-    def __init__(self, out=sys.stdout):
-        super(ArgparseWriter, self).__init__(out)
+
+    def __init__(self, prog, out=sys.stdout, aliases=False):
+        """Initializes a new ArgparseWriter instance.
+
+        Parameters:
+            prog (str): the program name
+            out (file object): the file to write to
+            aliases (bool): whether or not to include subparsers for aliases
+        """
+        super(ArgparseWriter, self).__init__(prog)
         self.level = 0
+        self.prog = prog
         self.out = out
+        self.aliases = aliases
+
+    def parse(self, parser, prog):
+        """Parses the parser object and returns the relavent components.
 
-    def _write(self, parser, root=True, level=0):
+        Parameters:
+            parser (argparse.ArgumentParser): the parser
+            prog (str): the command name
+
+        Returns:
+            (Command) information about the command from the parser
+        """
         self.parser = parser
-        self.level = level
-        actions = parser._actions
 
-        # allow root level to be flattened with rest of commands
-        if type(root) == int:
-            self.level = root
-            root = True
+        split_prog = parser.prog.split(' ')
+        split_prog[-1] = prog
+        prog = ' '.join(split_prog)
+        description = parser.description
+
+        fmt = parser._get_formatter()
+        actions = parser._actions
+        groups = parser._mutually_exclusive_groups
+        usage = fmt._format_usage(None, actions, groups, '').strip()
 
-        # go through actions and split them into optionals, positionals,
+        # Go through actions and split them into optionals, positionals,
         # and subcommands
         optionals = []
         positionals = []
         subcommands = []
         for action in actions:
             if action.option_strings:
-                optionals.append(action)
+                flags = action.option_strings
+                dest_flags = fmt._format_action_invocation(action)
+                help = self._expand_help(action) if action.help else ''
+                help = help.replace('\n', ' ')
+                optionals.append((flags, dest_flags, help))
             elif isinstance(action, argparse._SubParsersAction):
                 for subaction in action._choices_actions:
                     subparser = action._name_parser_map[subaction.dest]
-                    subcommands.append(subparser)
+                    subcommands.append((subparser, subaction.dest))
+
+                    # Look for aliases of the form 'name (alias, ...)'
+                    if self.aliases:
+                        match = re.match(r'(.*) \((.*)\)', subaction.metavar)
+                        if match:
+                            aliases = match.group(2).split(', ')
+                            for alias in aliases:
+                                subparser = action._name_parser_map[alias]
+                                subcommands.append((subparser, alias))
             else:
-                positionals.append(action)
+                args = fmt._format_action_invocation(action)
+                help = self._expand_help(action) if action.help else ''
+                help = help.replace('\n', ' ')
+                positionals.append((args, help))
 
-        groups = parser._mutually_exclusive_groups
-        fmt = parser._get_formatter()
-        description = parser.description
+        return Command(
+            prog, description, usage, positionals, optionals, subcommands)
 
-        def action_group(function, actions):
-            for action in actions:
-                arg = fmt._format_action_invocation(action)
-                help = self._expand_help(action) if action.help else ''
-                function(arg, re.sub('\n', ' ', help))
+    def format(self, cmd):
+        """Returns the string representation of a single node in the
+        parser tree.
 
-        if root:
-            self.begin_command(parser.prog)
+        Override this in subclasses to define how each subcommand
+        should be displayed.
 
-            if description:
-                self.description(parser.description)
+        Parameters:
+            (Command): parsed information about a command or subcommand
 
-            usage = fmt._format_usage(None, actions, groups, '').strip()
-            self.usage(usage)
+        Returns:
+            str: the string representation of this subcommand
+        """
+        raise NotImplementedError
 
-            if positionals:
-                self.begin_positionals()
-                action_group(self.positional, positionals)
-                self.end_positionals()
+    def _write(self, parser, prog, level=0):
+        """Recursively writes a parser.
 
-            if optionals:
-                self.begin_optionals()
-                action_group(self.optional, optionals)
-                self.end_optionals()
+        Parameters:
+            parser (argparse.ArgumentParser): the parser
+            prog (str): the command name
+            level (int): the current level
+        """
+        self.level = level
 
-        if subcommands:
-            self.begin_subcommands(subcommands)
-            for subparser in subcommands:
-                self._write(subparser, root=True, level=level + 1)
-            self.end_subcommands(subcommands)
+        cmd = self.parse(parser, prog)
+        self.out.write(self.format(cmd))
 
-        if root:
-            self.end_command(parser.prog)
+        for subparser, prog in cmd.subcommands:
+            self._write(subparser, prog, level=level + 1)
 
-    def write(self, parser, root=True):
+    def write(self, parser):
         """Write out details about an ArgumentParser.
 
         Args:
-            parser (ArgumentParser): an ``argparse`` parser
-            root (bool or int): if bool, whether to include the root parser;
-                or ``1`` to flatten the root parser with first-level
-                subcommands
+            parser (argparse.ArgumentParser): the parser
         """
         try:
-            self._write(parser, root, level=0)
+            self._write(parser, self.prog)
         except IOError as e:
-            # swallow pipe errors
+            # Swallow pipe errors
+            # Raises IOError in Python 2 and BrokenPipeError in Python 3
             if e.errno != errno.EPIPE:
                 raise
 
+
+_rst_levels = ['=', '-', '^', '~', ':', '`']
+
+
+class ArgparseRstWriter(ArgparseWriter):
+    """Write argparse output as rst sections."""
+
+    def __init__(self, prog, out=sys.stdout, aliases=False,
+                 rst_levels=_rst_levels):
+        """Create a new ArgparseRstWriter.
+
+        Parameters:
+            prog (str): program name
+            out (file object): file to write to
+            aliases (bool): whether or not to include subparsers for aliases
+            rst_levels (list of str): list of characters
+                for rst section headings
+        """
+        super(ArgparseRstWriter, self).__init__(prog, out, aliases)
+        self.rst_levels = rst_levels
+
+    def format(self, cmd):
+        string = StringIO()
+        string.write(self.begin_command(cmd.prog))
+
+        if cmd.description:
+            string.write(self.description(cmd.description))
+
+        string.write(self.usage(cmd.usage))
+
+        if cmd.positionals:
+            string.write(self.begin_positionals())
+            for args, help in cmd.positionals:
+                string.write(self.positional(args, help))
+            string.write(self.end_positionals())
+
+        if cmd.optionals:
+            string.write(self.begin_optionals())
+            for flags, dest_flags, help in cmd.optionals:
+                string.write(self.optional(dest_flags, help))
+            string.write(self.end_optionals())
+
+        if cmd.subcommands:
+            string.write(self.begin_subcommands(cmd.subcommands))
+
+        return string.getvalue()
+
     def begin_command(self, prog):
-        pass
+        return """
+----
+
+.. _{0}:
 
-    def end_command(self, prog):
-        pass
+{1}
+{2}
+
+""".format(prog.replace(' ', '-'), prog,
+           self.rst_levels[self.level] * len(prog))
 
     def description(self, description):
-        pass
+        return description + '\n\n'
 
     def usage(self, usage):
-        pass
+        return """\
+.. code-block:: console
+
+    {0}
+
+""".format(usage)
 
     def begin_positionals(self):
-        pass
+        return '\n**Positional arguments**\n\n'
 
     def positional(self, name, help):
-        pass
+        return """\
+{0}
+  {1}
+
+""".format(name, help)
 
     def end_positionals(self):
-        pass
+        return ''
 
     def begin_optionals(self):
-        pass
+        return '\n**Optional arguments**\n\n'
+
+    def optional(self, opts, help):
+        return """\
+``{0}``
+  {1}
 
-    def optional(self, option, help):
-        pass
+""".format(opts, help)
 
     def end_optionals(self):
-        pass
+        return ''
 
     def begin_subcommands(self, subcommands):
-        pass
+        string = """
+**Subcommands**
 
-    def end_subcommands(self, subcommands):
-        pass
+.. hlist::
+   :columns: 4
 
+"""
 
-_rst_levels = ['=', '-', '^', '~', ':', '`']
+        for cmd, _ in subcommands:
+            prog = re.sub(r'^[^ ]* ', '', cmd.prog)
+            string += '   * :ref:`{0} <{1}>`\n'.format(
+                prog, cmd.prog.replace(' ', '-'))
 
+        return string + '\n'
 
-class ArgparseRstWriter(ArgparseWriter):
-    """Write argparse output as rst sections."""
 
-    def __init__(self, out=sys.stdout, rst_levels=_rst_levels,
-                 strip_root_prog=True):
-        """Create a new ArgparseRstWriter.
+class ArgparseCompletionWriter(ArgparseWriter):
+    """Write argparse output as shell programmable tab completion functions."""
 
-        Args:
-            out (file object): file to write to
-            rst_levels (list of str): list of characters
-                for rst section headings
-            strip_root_prog (bool): if ``True``, strip the base command name
-                from subcommands in output
+    def format(self, cmd):
+        """Returns the string representation of a single node in the
+        parser tree.
+
+        Override this in subclasses to define how each subcommand
+        should be displayed.
+
+        Parameters:
+            (Command): parsed information about a command or subcommand
+
+        Returns:
+            str: the string representation of this subcommand
         """
-        super(ArgparseRstWriter, self).__init__(out)
-        self.rst_levels = rst_levels
-        self.strip_root_prog = strip_root_prog
 
-    def line(self, string=''):
-        self.out.write('%s\n' % string)
+        assert cmd.optionals  # we should always at least have -h, --help
+        assert not (cmd.positionals and cmd.subcommands)  # one or the other
 
-    def begin_command(self, prog):
-        self.line()
-        self.line('----')
-        self.line()
-        self.line('.. _%s:\n' % prog.replace(' ', '-'))
-        self.line('%s' % prog)
-        self.line(self.rst_levels[self.level] * len(prog) + '\n')
+        # We only care about the arguments/flags, not the help messages
+        positionals = []
+        if cmd.positionals:
+            positionals, _ = zip(*cmd.positionals)
+        optionals, _, _ = zip(*cmd.optionals)
+        subcommands = []
+        if cmd.subcommands:
+            _, subcommands = zip(*cmd.subcommands)
 
-    def description(self, description):
-        self.line('%s\n' % description)
+        # Flatten lists of lists
+        optionals = [x for xx in optionals for x in xx]
 
-    def usage(self, usage):
-        self.line('.. code-block:: console\n')
-        self.line('    %s\n' % usage)
+        return (self.start_function(cmd.prog) +
+                self.body(positionals, optionals, subcommands) +
+                self.end_function(cmd.prog))
 
-    def begin_positionals(self):
-        self.line()
-        self.line('**Positional arguments**\n')
+    def start_function(self, prog):
+        """Returns the syntax needed to begin a function definition.
 
-    def positional(self, name, help):
-        self.line(name)
-        self.line('  %s\n' % help)
+        Parameters:
+            prog (str): the command name
 
-    def begin_optionals(self):
-        self.line()
-        self.line('**Optional arguments**\n')
+        Returns:
+            str: the function definition beginning
+        """
+        name = prog.replace('-', '_').replace(' ', '_')
+        return '\n_{0}() {{'.format(name)
 
-    def optional(self, opts, help):
-        self.line('``%s``' % opts)
-        self.line('  %s\n' % help)
+    def end_function(self, prog=None):
+        """Returns the syntax needed to end a function definition.
 
-    def begin_subcommands(self, subcommands):
-        self.line()
-        self.line('**Subcommands**\n')
-        self.line('.. hlist::')
-        self.line('   :columns: 4\n')
-
-        for cmd in subcommands:
-            prog = cmd.prog
-            if self.strip_root_prog:
-                prog = re.sub(r'^[^ ]* ', '', prog)
-
-            self.line('   * :ref:`%s <%s>`'
-                      % (prog, cmd.prog.replace(' ', '-')))
-        self.line()
+        Parameters:
+            prog (str, optional): the command name
+
+        Returns:
+            str: the function definition ending
+        """
+        return '}\n'
+
+    def body(self, positionals, optionals, subcommands):
+        """Returns the body of the function.
+
+        Parameters:
+            positionals (list): list of positional arguments
+            optionals (list): list of optional arguments
+            subcommands (list): list of subcommand parsers
+
+        Returns:
+            str: the function body
+        """
+        return ''
+
+    def positionals(self, positionals):
+        """Returns the syntax for reporting positional arguments.
+
+        Parameters:
+            positionals (list): list of positional arguments
+
+        Returns:
+            str: the syntax for positional arguments
+        """
+        return ''
+
+    def optionals(self, optionals):
+        """Returns the syntax for reporting optional flags.
+
+        Parameters:
+            optionals (list): list of optional arguments
+
+        Returns:
+            str: the syntax for optional flags
+        """
+        return ''
+
+    def subcommands(self, subcommands):
+        """Returns the syntax for reporting subcommands.
+
+        Parameters:
+            subcommands (list): list of subcommand parsers
+
+        Returns:
+            str: the syntax for subcommand parsers
+        """
+        return ''
diff --git a/lib/spack/spack/cmd/activate.py b/lib/spack/spack/cmd/activate.py
index 718d30ce07cf396affeeded2c1b7ebf272c4a1ea..bfca9f56045c1a7232f4c068b47bc1888b05128f 100644
--- a/lib/spack/spack/cmd/activate.py
+++ b/lib/spack/spack/cmd/activate.py
@@ -3,11 +3,10 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
-
 import llnl.util.tty as tty
 
 import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.environment as ev
 from spack.filesystem_view import YamlFilesystemView
 
@@ -23,9 +22,7 @@ def setup_parser(subparser):
     subparser.add_argument(
         '-v', '--view', metavar='VIEW', type=str,
         help="the view to operate on")
-    subparser.add_argument(
-        'spec', nargs=argparse.REMAINDER,
-        help="spec of package extension to activate")
+    arguments.add_common_arguments(subparser, ['installed_spec'])
 
 
 def activate(parser, args):
diff --git a/lib/spack/spack/cmd/add.py b/lib/spack/spack/cmd/add.py
index efae7ffeb7e43441ee6bb796ca15bdb3f866468b..e08c2c5aacc68f6283558c120ca308b1893caf5c 100644
--- a/lib/spack/spack/cmd/add.py
+++ b/lib/spack/spack/cmd/add.py
@@ -3,11 +3,10 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
-
 import llnl.util.tty as tty
 
 import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.environment as ev
 
 
@@ -20,8 +19,7 @@ def setup_parser(subparser):
     subparser.add_argument('-l', '--list-name',
                            dest='list_name', default='specs',
                            help="name of the list to add specs to")
-    subparser.add_argument(
-        'specs', nargs=argparse.REMAINDER, help="specs of packages to add")
+    arguments.add_common_arguments(subparser, ['specs'])
 
 
 def add(parser, args):
diff --git a/lib/spack/spack/cmd/blame.py b/lib/spack/spack/cmd/blame.py
index ea1a310476a6019045fb87487ebca61345077e3f..b806058aec3fbf3e80d6e9f2e7981dfb05e1cae3 100644
--- a/lib/spack/spack/cmd/blame.py
+++ b/lib/spack/spack/cmd/blame.py
@@ -35,7 +35,7 @@ def setup_parser(subparser):
         help='show git blame output instead of summary')
 
     subparser.add_argument(
-        'package_name', help='name of package to show contributions for, '
+        'package_or_file', help='name of package to show contributions for, '
         'or path to a file in the spack repo')
 
 
@@ -47,13 +47,13 @@ def blame(parser, args):
 
     # Get name of file to blame
     blame_file = None
-    if os.path.isfile(args.package_name):
-        path = os.path.realpath(args.package_name)
+    if os.path.isfile(args.package_or_file):
+        path = os.path.realpath(args.package_or_file)
         if path.startswith(spack.paths.prefix):
             blame_file = path
 
     if not blame_file:
-        pkg = spack.repo.get(args.package_name)
+        pkg = spack.repo.get(args.package_or_file)
         blame_file = pkg.module.__file__.rstrip('c')  # .pyc -> .py
 
     # get git blame for the package
diff --git a/lib/spack/spack/cmd/build_env.py b/lib/spack/spack/cmd/build_env.py
index 7f9f08c01f73f747cc9c1c1c2f1d42f08f32cd46..128d167a291bda9a07484ca29a89dc33a78e4ba8 100644
--- a/lib/spack/spack/cmd/build_env.py
+++ b/lib/spack/spack/cmd/build_env.py
@@ -33,7 +33,7 @@ def setup_parser(subparser):
     subparser.add_argument(
         'spec', nargs=argparse.REMAINDER,
         metavar='spec [--] [cmd]...',
-        help="specs of package environment to emulate")
+        help="spec of package environment to emulate")
     subparser.epilog\
         = 'If a command is not specified, the environment will be printed ' \
         'to standard output (cf /usr/bin/env) unless --dump and/or --pickle ' \
diff --git a/lib/spack/spack/cmd/buildcache.py b/lib/spack/spack/cmd/buildcache.py
index 5baa63af85f63aa340fbc5b92b271215c9359efc..cbcbc2c0cb4d5f42633e4915985ef8bce5e16e6f 100644
--- a/lib/spack/spack/cmd/buildcache.py
+++ b/lib/spack/spack/cmd/buildcache.py
@@ -3,7 +3,6 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
 import os
 import shutil
 import sys
@@ -61,11 +60,9 @@ def setup_parser(subparser):
                                             "building package(s)")
     create.add_argument('-y', '--spec-yaml', default=None,
                         help='Create buildcache entry for spec from yaml file')
-    create.add_argument(
-        'packages', nargs=argparse.REMAINDER,
-        help="specs of packages to create buildcache for")
     create.add_argument('--no-deps', action='store_true', default='false',
                         help='Create buildcache entry wo/ dependencies')
+    arguments.add_common_arguments(create, ['specs'])
     create.set_defaults(func=createtarball)
 
     install = subparsers.add_parser('install', help=installtarball.__doc__)
@@ -79,9 +76,7 @@ def setup_parser(subparser):
     install.add_argument('-u', '--unsigned', action='store_true',
                          help="install unsigned buildcache" +
                               " tarballs for testing")
-    install.add_argument(
-        'packages', nargs=argparse.REMAINDER,
-        help="specs of packages to install buildcache for")
+    arguments.add_common_arguments(install, ['specs'])
     install.set_defaults(func=installtarball)
 
     listcache = subparsers.add_parser('list', help=listspecs.__doc__)
@@ -92,9 +87,7 @@ def setup_parser(subparser):
                            help='show variants in output (can be long)')
     listcache.add_argument('-f', '--force', action='store_true',
                            help="force new download of specs")
-    listcache.add_argument(
-        'packages', nargs=argparse.REMAINDER,
-        help="specs of packages to search for")
+    arguments.add_common_arguments(listcache, ['specs'])
     listcache.set_defaults(func=listspecs)
 
     dlkeys = subparsers.add_parser('keys', help=getkeys.__doc__)
@@ -113,10 +106,9 @@ def setup_parser(subparser):
         help='analyzes an installed spec and reports whether '
              'executables and libraries are relocatable'
     )
-    preview_parser.add_argument(
-        'packages', nargs='+', help='list of installed packages'
-    )
+    arguments.add_common_arguments(preview_parser, ['installed_specs'])
     preview_parser.set_defaults(func=preview)
+
     # Check if binaries need to be rebuilt on remote mirror
     check = subparsers.add_parser('check', help=check_binaries.__doc__)
     check.add_argument(
@@ -313,8 +305,10 @@ def _createtarball(env, spec_yaml, packages, directory, key, no_deps, force,
             tty.debug(yaml_text)
             s = Spec.from_yaml(yaml_text)
             packages.add('/{0}'.format(s.dag_hash()))
+
     elif packages:
         packages = packages
+
     else:
         tty.die("build cache file creation requires at least one" +
                 " installed package argument or else path to a" +
@@ -378,17 +372,17 @@ def createtarball(args):
     # restrict matching to current environment if one is active
     env = ev.get_env(args, 'buildcache create')
 
-    _createtarball(env, args.spec_yaml, args.packages, args.directory,
+    _createtarball(env, args.spec_yaml, args.specs, args.directory,
                    args.key, args.no_deps, args.force, args.rel, args.unsigned,
                    args.allow_root, args.no_rebuild_index)
 
 
 def installtarball(args):
     """install from a binary package"""
-    if not args.packages:
+    if not args.specs:
         tty.die("build cache file installation requires" +
                 " at least one package spec argument")
-    pkgs = set(args.packages)
+    pkgs = set(args.specs)
     matches = match_downloaded_specs(pkgs, args.multiple, args.force)
 
     for match in matches:
@@ -422,8 +416,8 @@ def install_tarball(spec, args):
 def listspecs(args):
     """list binary packages available from mirrors"""
     specs = bindist.get_specs(args.force)
-    if args.packages:
-        constraints = set(args.packages)
+    if args.specs:
+        constraints = set(args.specs)
         specs = [s for s in specs if any(s.satisfies(c) for c in constraints)]
     display_specs(specs, args, all_headers=True)
 
@@ -440,7 +434,7 @@ def preview(args):
     Args:
         args: command line arguments
     """
-    specs = find_matching_specs(args.packages, allow_multiple_matches=True)
+    specs = find_matching_specs(args.specs, allow_multiple_matches=True)
 
     # Cycle over the specs that match
     for spec in specs:
diff --git a/lib/spack/spack/cmd/checksum.py b/lib/spack/spack/cmd/checksum.py
index c606cd3886974fb58430f8a2cbd89a5655d06390..343915868cdfe31d4e4a4869eecb4e9a13d46c9b 100644
--- a/lib/spack/spack/cmd/checksum.py
+++ b/lib/spack/spack/cmd/checksum.py
@@ -10,6 +10,7 @@
 import llnl.util.tty as tty
 
 import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.repo
 import spack.stage
 import spack.util.crypto
@@ -22,12 +23,10 @@
 
 
 def setup_parser(subparser):
-    subparser.add_argument(
-        'package',
-        help='package to checksum versions for')
     subparser.add_argument(
         '--keep-stage', action='store_true',
         help="don't clean up staging area when command completes")
+    arguments.add_common_arguments(subparser, ['package'])
     subparser.add_argument(
         'versions', nargs=argparse.REMAINDER,
         help='versions to generate checksums for')
diff --git a/lib/spack/spack/cmd/clean.py b/lib/spack/spack/cmd/clean.py
index dc25857b516a5d49a712a0df6e4fa4c1d3517aca..791a1b7dc3138008895a9a29073395061ddb1d39 100644
--- a/lib/spack/spack/cmd/clean.py
+++ b/lib/spack/spack/cmd/clean.py
@@ -11,6 +11,7 @@
 
 import spack.caches
 import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.repo
 import spack.stage
 from spack.paths import lib_path, var_path
@@ -43,11 +44,7 @@ def setup_parser(subparser):
     subparser.add_argument(
         '-a', '--all', action=AllClean, help="equivalent to -sdmp", nargs=0
     )
-    subparser.add_argument(
-        'specs',
-        nargs=argparse.REMAINDER,
-        help="removes the build stages and tarballs for specs"
-    )
+    arguments.add_common_arguments(subparser, ['specs'])
 
 
 def clean(parser, args):
diff --git a/lib/spack/spack/cmd/commands.py b/lib/spack/spack/cmd/commands.py
index b4fde1aea4dc391d8e3c836f5b8e9a942bcdcc07..4966bd7858ad6d894c3dbfab44afe5c8b3569598 100644
--- a/lib/spack/spack/cmd/commands.py
+++ b/lib/spack/spack/cmd/commands.py
@@ -5,13 +5,16 @@
 
 from __future__ import print_function
 
-import sys
+import argparse
+import copy
 import os
 import re
-import argparse
+import sys
 
 import llnl.util.tty as tty
-from llnl.util.argparsewriter import ArgparseWriter, ArgparseRstWriter
+from llnl.util.argparsewriter import (
+    ArgparseWriter, ArgparseRstWriter, ArgparseCompletionWriter
+)
 from llnl.util.tty.colify import colify
 
 import spack.cmd
@@ -35,6 +38,8 @@ def formatter(func):
 
 
 def setup_parser(subparser):
+    subparser.add_argument(
+        '-a', '--aliases', action='store_true', help='include command aliases')
     subparser.add_argument(
         '--format', default='names', choices=formatters,
         help='format to be used to print the output (default: names)')
@@ -52,29 +57,97 @@ def setup_parser(subparser):
 class SpackArgparseRstWriter(ArgparseRstWriter):
     """RST writer tailored for spack documentation."""
 
-    def __init__(self, documented_commands, out=sys.stdout):
-        super(SpackArgparseRstWriter, self).__init__(out)
-        self.documented = documented_commands if documented_commands else []
+    def __init__(self, prog, out=sys.stdout, aliases=False,
+                 documented_commands=[],
+                 rst_levels=['-', '-', '^', '~', ':', '`']):
+        super(SpackArgparseRstWriter, self).__init__(
+            prog, out, aliases, rst_levels)
+        self.documented = documented_commands
 
     def usage(self, *args):
-        super(SpackArgparseRstWriter, self).usage(*args)
-        cmd = re.sub(' ', '-', self.parser.prog)
+        string = super(SpackArgparseRstWriter, self).usage(*args)
+
+        cmd = self.parser.prog.replace(' ', '-')
         if cmd in self.documented:
-            self.line()
-            self.line(':ref:`More documentation <cmd-%s>`' % cmd)
+            string += '\n:ref:`More documentation <cmd-{0}>`\n'.format(cmd)
+
+        return string
 
 
 class SubcommandWriter(ArgparseWriter):
-    def begin_command(self, prog):
-        self.out.write('    ' * self.level + prog)
-        self.out.write('\n')
+    def format(self, cmd):
+        return '    ' * self.level + cmd.prog + '\n'
+
+
+_positional_to_subroutine = {
+    'package': '_all_packages',
+    'spec': '_all_packages',
+    'filter': '_all_packages',
+    'installed': '_installed_packages',
+    'compiler': '_installed_compilers',
+    'section': '_config_sections',
+    'env': '_environments',
+    'extendable': '_extensions',
+    'keys': '_keys',
+    'help_command': '_subcommands',
+    'mirror': '_mirrors',
+    'virtual': '_providers',
+    'namespace': '_repos',
+    'hash': '_all_resource_hashes',
+    'pytest': '_tests',
+}
+
+
+class BashCompletionWriter(ArgparseCompletionWriter):
+    """Write argparse output as bash programmable tab completion."""
+
+    def body(self, positionals, optionals, subcommands):
+        if positionals:
+            return """
+    if $list_options
+    then
+        {0}
+    else
+        {1}
+    fi
+""".format(self.optionals(optionals), self.positionals(positionals))
+        elif subcommands:
+            return """
+    if $list_options
+    then
+        {0}
+    else
+        {1}
+    fi
+""".format(self.optionals(optionals), self.subcommands(subcommands))
+        else:
+            return """
+    {0}
+""".format(self.optionals(optionals))
+
+    def positionals(self, positionals):
+        # If match found, return function name
+        for positional in positionals:
+            for key, value in _positional_to_subroutine.items():
+                if positional.startswith(key):
+                    return value
+
+        # If no matches found, return empty list
+        return 'SPACK_COMPREPLY=""'
+
+    def optionals(self, optionals):
+        return 'SPACK_COMPREPLY="{0}"'.format(' '.join(optionals))
+
+    def subcommands(self, subcommands):
+        return 'SPACK_COMPREPLY="{0}"'.format(' '.join(subcommands))
 
 
 @formatter
 def subcommands(args, out):
     parser = spack.main.make_argument_parser()
     spack.main.add_all_commands(parser)
-    SubcommandWriter(out).write(parser)
+    writer = SubcommandWriter(parser.prog, out, args.aliases)
+    writer.write(parser)
 
 
 def rst_index(out):
@@ -124,12 +197,28 @@ def rst(args, out):
     out.write('\n')
 
     # print sections for each command and subcommand
-    SpackArgparseRstWriter(documented_commands, out).write(parser, root=1)
+    writer = SpackArgparseRstWriter(
+        parser.prog, out, args.aliases, documented_commands)
+    writer.write(parser)
 
 
 @formatter
 def names(args, out):
-    colify(spack.cmd.all_commands(), output=out)
+    commands = copy.copy(spack.cmd.all_commands())
+
+    if args.aliases:
+        commands.extend(spack.main.aliases.keys())
+
+    colify(commands, output=out)
+
+
+@formatter
+def bash(args, out):
+    parser = spack.main.make_argument_parser()
+    spack.main.add_all_commands(parser)
+
+    writer = BashCompletionWriter(parser.prog, out, args.aliases)
+    writer.write(parser)
 
 
 def prepend_header(args, out):
@@ -148,12 +237,14 @@ def commands(parser, args):
         tty.die("No such file: '%s'" % args.header)
 
     # if we're updating an existing file, only write output if a command
-    # is newer than the file.
+    # or the header is newer than the file.
     if args.update:
         if os.path.exists(args.update):
             files = [
                 spack.cmd.get_module(command).__file__.rstrip('c')  # pyc -> py
                 for command in spack.cmd.all_commands()]
+            if args.header:
+                files.append(args.header)
             last_update = os.path.getmtime(args.update)
             if not any(os.path.getmtime(f) > last_update for f in files):
                 tty.msg('File is up to date: %s' % args.update)
diff --git a/lib/spack/spack/cmd/common/arguments.py b/lib/spack/spack/cmd/common/arguments.py
index da8b0b0277b253ee5cc346f0df08e32632cc3d64..b93f265c7ab20ac7268d8d54a4f2288e3a41871a 100644
--- a/lib/spack/spack/cmd/common/arguments.py
+++ b/lib/spack/spack/cmd/common/arguments.py
@@ -120,7 +120,7 @@ def default(self, value):
 
 
 class DeptypeAction(argparse.Action):
-    """Creates a tuple of valid dependency tpyes from a deptype argument."""
+    """Creates a tuple of valid dependency types from a deptype argument."""
     def __call__(self, parser, namespace, values, option_string=None):
         deptype = dep.all_deptypes
         if values:
@@ -132,11 +132,53 @@ def __call__(self, parser, namespace, values, option_string=None):
         setattr(namespace, self.dest, deptype)
 
 
+# TODO: merge constraint and installed_specs
 @arg
 def constraint():
     return Args(
         'constraint', nargs=argparse.REMAINDER, action=ConstraintAction,
-        help='constraint to select a subset of installed packages')
+        help='constraint to select a subset of installed packages',
+        metavar='installed_specs')
+
+
+@arg
+def package():
+    return Args('package', help='package name')
+
+
+@arg
+def packages():
+    return Args(
+        'packages', nargs='+', help='one or more package names',
+        metavar='package')
+
+
+# Specs must use `nargs=argparse.REMAINDER` because a single spec can
+# contain spaces, and contain variants like '-mpi' that argparse thinks
+# are a collection of optional flags.
+@arg
+def spec():
+    return Args('spec', nargs=argparse.REMAINDER, help='package spec')
+
+
+@arg
+def specs():
+    return Args(
+        'specs', nargs=argparse.REMAINDER, help='one or more package specs')
+
+
+@arg
+def installed_spec():
+    return Args(
+        'spec', nargs=argparse.REMAINDER, help='installed package spec',
+        metavar='installed_spec')
+
+
+@arg
+def installed_specs():
+    return Args(
+        'specs', nargs=argparse.REMAINDER,
+        help='one or more installed package specs', metavar='installed_specs')
 
 
 @arg
diff --git a/lib/spack/spack/cmd/config.py b/lib/spack/spack/cmd/config.py
index 9951f346cbd05ee811d3dc344b155d8aa2006f68..b6055a7f6b66cd4a597d624d20803354c3b46ca2 100644
--- a/lib/spack/spack/cmd/config.py
+++ b/lib/spack/spack/cmd/config.py
@@ -34,7 +34,7 @@ def setup_parser(subparser):
                             help="configuration section to print. "
                                  "options: %(choices)s",
                             nargs='?',
-                            metavar='SECTION',
+                            metavar='section',
                             choices=spack.config.section_schemas)
 
     blame_parser = sp.add_parser(
@@ -42,14 +42,14 @@ def setup_parser(subparser):
     blame_parser.add_argument('section',
                               help="configuration section to print. "
                               "options: %(choices)s",
-                              metavar='SECTION',
+                              metavar='section',
                               choices=spack.config.section_schemas)
 
     edit_parser = sp.add_parser('edit', help='edit configuration file')
     edit_parser.add_argument('section',
                              help="configuration section to edit. "
                                   "options: %(choices)s",
-                             metavar='SECTION',
+                             metavar='section',
                              nargs='?',
                              choices=spack.config.section_schemas)
     edit_parser.add_argument(
diff --git a/lib/spack/spack/cmd/configure.py b/lib/spack/spack/cmd/configure.py
index d15d2be91219e4876930e44e1c7368cc2ce3b946..3df3c874136cd7ff99b326f8b26df3d2ba9ce718 100644
--- a/lib/spack/spack/cmd/configure.py
+++ b/lib/spack/spack/cmd/configure.py
@@ -7,6 +7,7 @@
 
 import llnl.util.tty as tty
 import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.cmd.install as inst
 
 from spack.build_systems.autotools import AutotoolsPackage
@@ -36,16 +37,12 @@
 
 
 def setup_parser(subparser):
-    subparser.add_argument(
-        'package',
-        nargs=argparse.REMAINDER,
-        help="spec of the package to install"
-    )
     subparser.add_argument(
         '-v', '--verbose',
         action='store_true',
         help="print additional output during builds"
     )
+    arguments.add_common_arguments(subparser, ['spec'])
 
 
 def _stop_at_phase_during_install(args, calling_fn, phase_mapping):
@@ -64,15 +61,15 @@ def _stop_at_phase_during_install(args, calling_fn, phase_mapping):
         # Install package dependencies if needed
         parser = argparse.ArgumentParser()
         inst.setup_parser(parser)
-        tty.msg('Checking dependencies for {0}'.format(args.package[0]))
+        tty.msg('Checking dependencies for {0}'.format(args.spec[0]))
         cli_args = ['-v'] if args.verbose else []
         install_args = parser.parse_args(cli_args + ['--only=dependencies'])
-        install_args.package = args.package
+        install_args.spec = args.spec
         inst.install(parser, install_args)
         # Install package and stop at the given phase
         cli_args = ['-v'] if args.verbose else []
         install_args = parser.parse_args(cli_args + ['--only=package'])
-        install_args.package = args.package
+        install_args.spec = args.spec
         inst.install(parser, install_args, stop_at=phase)
     except IndexError:
         tty.error(
diff --git a/lib/spack/spack/cmd/deactivate.py b/lib/spack/spack/cmd/deactivate.py
index 43ef09a9b16c5950c4e45fdf779a46b836ee46b4..3c72531a9c8abddeeff6600bd1508832869ae22d 100644
--- a/lib/spack/spack/cmd/deactivate.py
+++ b/lib/spack/spack/cmd/deactivate.py
@@ -3,10 +3,10 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
 import llnl.util.tty as tty
 
 import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.environment as ev
 import spack.store
 from spack.filesystem_view import YamlFilesystemView
@@ -28,9 +28,7 @@ def setup_parser(subparser):
         '-a', '--all', action='store_true',
         help="deactivate all extensions of an extendable package, or "
         "deactivate an extension AND its dependencies")
-    subparser.add_argument(
-        'spec', nargs=argparse.REMAINDER,
-        help="spec of package extension to deactivate")
+    arguments.add_common_arguments(subparser, ['installed_spec'])
 
 
 def deactivate(parser, args):
diff --git a/lib/spack/spack/cmd/dependencies.py b/lib/spack/spack/cmd/dependencies.py
index db8fbe4b48fdd73f1998a30a35efb1da73d4f7e5..e65e050bfacb509fa18601f52934073ce24b6151 100644
--- a/lib/spack/spack/cmd/dependencies.py
+++ b/lib/spack/spack/cmd/dependencies.py
@@ -3,8 +3,6 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
-
 import llnl.util.tty as tty
 from llnl.util.tty.colify import colify
 
@@ -31,8 +29,7 @@ def setup_parser(subparser):
     subparser.add_argument(
         '-V', '--no-expand-virtuals', action='store_false', default=True,
         dest="expand_virtuals", help="do not expand virtual dependencies")
-    subparser.add_argument(
-        'spec', nargs=argparse.REMAINDER, help="spec or package name")
+    arguments.add_common_arguments(subparser, ['spec'])
 
 
 def dependencies(parser, args):
diff --git a/lib/spack/spack/cmd/dependents.py b/lib/spack/spack/cmd/dependents.py
index 78e862a982c7973ae9ec98abf74086bae46f7ff2..e60733f589081ec82627541c9c5c28ca712bdaad 100644
--- a/lib/spack/spack/cmd/dependents.py
+++ b/lib/spack/spack/cmd/dependents.py
@@ -3,15 +3,14 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
-
 import llnl.util.tty as tty
 from llnl.util.tty.colify import colify
 
+import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.environment as ev
 import spack.repo
 import spack.store
-import spack.cmd
 
 description = "show packages that depend on another"
 section = "basic"
@@ -26,8 +25,7 @@ def setup_parser(subparser):
     subparser.add_argument(
         '-t', '--transitive', action='store_true', default=False,
         help="Show all transitive dependents.")
-    subparser.add_argument(
-        'spec', nargs=argparse.REMAINDER, help="spec or package name")
+    arguments.add_common_arguments(subparser, ['spec'])
 
 
 def inverted_dependencies():
diff --git a/lib/spack/spack/cmd/dev_build.py b/lib/spack/spack/cmd/dev_build.py
index 190720b05f4c37fd46f3763fa64660d78d963d92..c1004f24b38b959b0cd10b67c9f08b0b309d32ad 100644
--- a/lib/spack/spack/cmd/dev_build.py
+++ b/lib/spack/spack/cmd/dev_build.py
@@ -5,14 +5,13 @@
 
 import sys
 import os
-import argparse
 
 import llnl.util.tty as tty
 
 import spack.config
 import spack.cmd
-import spack.repo
 import spack.cmd.common.arguments as arguments
+import spack.repo
 from spack.stage import DIYStage
 
 description = "developer build: build from code in current working directory"
@@ -41,9 +40,7 @@ def setup_parser(subparser):
     subparser.add_argument(
         '-u', '--until', type=str, dest='until', default=None,
         help="phase to stop after when installing (default None)")
-    subparser.add_argument(
-        'spec', nargs=argparse.REMAINDER,
-        help="specs to use for install. must contain package AND version")
+    arguments.add_common_arguments(subparser, ['spec'])
 
     cd_group = subparser.add_mutually_exclusive_group()
     arguments.add_common_arguments(cd_group, ['clean', 'dirty'])
diff --git a/lib/spack/spack/cmd/edit.py b/lib/spack/spack/cmd/edit.py
index 1438383b2c37157096fe97a95acec73c2ea5dabd..6cdc3b788d220909462f36bd9df8f8870ee22f1b 100644
--- a/lib/spack/spack/cmd/edit.py
+++ b/lib/spack/spack/cmd/edit.py
@@ -84,12 +84,11 @@ def setup_parser(subparser):
         help="namespace of package to edit")
 
     subparser.add_argument(
-        'name', nargs='?', default=None,
-        help="name of package to edit")
+        'package', nargs='?', default=None, help="package name")
 
 
 def edit(parser, args):
-    name = args.name
+    name = args.package
 
     # By default, edit package files
     path = spack.paths.packages_path
diff --git a/lib/spack/spack/cmd/env.py b/lib/spack/spack/cmd/env.py
index 1d61dfc250a9525c7b252198b23162652f38038b..a8bc1e5bbea45d868dc403d48e6d26cecec0b1df 100644
--- a/lib/spack/spack/cmd/env.py
+++ b/lib/spack/spack/cmd/env.py
@@ -157,7 +157,7 @@ def env_deactivate(args):
 def env_create_setup_parser(subparser):
     """create a new environment"""
     subparser.add_argument(
-        'create_env', metavar='ENV', help='name of environment to create')
+        'create_env', metavar='env', help='name of environment to create')
     subparser.add_argument(
         '-d', '--dir', action='store_true',
         help='create an environment in a specific directory')
@@ -221,7 +221,7 @@ def _env_create(name_or_path, init_file=None, dir=False, with_view=None):
 def env_remove_setup_parser(subparser):
     """remove an existing environment"""
     subparser.add_argument(
-        'rm_env', metavar='ENV', nargs='+',
+        'rm_env', metavar='env', nargs='+',
         help='environment(s) to remove')
     arguments.add_common_arguments(subparser, ['yes_to_all'])
 
diff --git a/lib/spack/spack/cmd/extensions.py b/lib/spack/spack/cmd/extensions.py
index 7e3db6638413a2dc674af45046f748ac4fa48af9..e834d7fd18646b1f51eacb71e70054f75decc17d 100644
--- a/lib/spack/spack/cmd/extensions.py
+++ b/lib/spack/spack/cmd/extensions.py
@@ -37,7 +37,7 @@ def setup_parser(subparser):
 
     subparser.add_argument(
         'spec', nargs=argparse.REMAINDER,
-        help='spec of package to list extensions for')
+        help='spec of package to list extensions for', metavar='extendable')
 
 
 def extensions(parser, args):
diff --git a/lib/spack/spack/cmd/fetch.py b/lib/spack/spack/cmd/fetch.py
index 3004250f0390efd4b319f3828a467dbb3ae3f65e..b91eb52ab874008a813278b44cfd455719723446 100644
--- a/lib/spack/spack/cmd/fetch.py
+++ b/lib/spack/spack/cmd/fetch.py
@@ -3,14 +3,12 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
-
 import llnl.util.tty as tty
 
 import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.config
 import spack.repo
-import spack.cmd.common.arguments as arguments
 
 description = "fetch archives for packages"
 section = "build"
@@ -25,19 +23,17 @@ def setup_parser(subparser):
     subparser.add_argument(
         '-D', '--dependencies', action='store_true',
         help="also fetch all dependencies")
-    subparser.add_argument(
-        'packages', nargs=argparse.REMAINDER,
-        help="specs of packages to fetch")
+    arguments.add_common_arguments(subparser, ['specs'])
 
 
 def fetch(parser, args):
-    if not args.packages:
+    if not args.specs:
         tty.die("fetch requires at least one package argument")
 
     if args.no_checksum:
         spack.config.set('config:checksum', False, scope='command_line')
 
-    specs = spack.cmd.parse_specs(args.packages, concretize=True)
+    specs = spack.cmd.parse_specs(args.specs, concretize=True)
     for spec in specs:
         if args.missing or args.dependencies:
             for s in spec.traverse():
diff --git a/lib/spack/spack/cmd/gpg.py b/lib/spack/spack/cmd/gpg.py
index c1a0cafe456e782488cec703dbad5e18c27d6b98..0a77812c1224bf97c7cfa5eb3101d88427eaa783 100644
--- a/lib/spack/spack/cmd/gpg.py
+++ b/lib/spack/spack/cmd/gpg.py
@@ -6,6 +6,7 @@
 import os
 import argparse
 
+import spack.cmd.common.arguments as arguments
 import spack.paths
 from spack.util.gpg import Gpg
 
@@ -19,8 +20,7 @@ def setup_parser(subparser):
     subparsers = subparser.add_subparsers(help='GPG sub-commands')
 
     verify = subparsers.add_parser('verify', help=gpg_verify.__doc__)
-    verify.add_argument('package', type=str,
-                        help='the package to verify')
+    arguments.add_common_arguments(verify, ['installed_spec'])
     verify.add_argument('signature', type=str, nargs='?',
                         help='the signature file')
     verify.set_defaults(func=gpg_verify)
@@ -44,8 +44,7 @@ def setup_parser(subparser):
                       help='the key to use for signing')
     sign.add_argument('--clearsign', action='store_true',
                       help='if specified, create a clearsign signature')
-    sign.add_argument('package', type=str,
-                      help='the package to sign')
+    arguments.add_common_arguments(sign, ['installed_spec'])
     sign.set_defaults(func=gpg_sign)
 
     create = subparsers.add_parser('create', help=gpg_create.__doc__)
@@ -122,9 +121,9 @@ def gpg_sign(args):
                                'please choose one')
     output = args.output
     if not output:
-        output = args.package + '.asc'
+        output = args.spec[0] + '.asc'
     # TODO: Support the package format Spack creates.
-    Gpg.sign(key, args.package, output, args.clearsign)
+    Gpg.sign(key, ' '.join(args.spec), output, args.clearsign)
 
 
 def gpg_trust(args):
@@ -155,8 +154,8 @@ def gpg_verify(args):
     # TODO: Support the package format Spack creates.
     signature = args.signature
     if signature is None:
-        signature = args.package + '.asc'
-    Gpg.verify(signature, args.package)
+        signature = args.spec[0] + '.asc'
+    Gpg.verify(signature, ' '.join(args.spec))
 
 
 def gpg(parser, args):
diff --git a/lib/spack/spack/cmd/graph.py b/lib/spack/spack/cmd/graph.py
index 94197283b8a2ca9cfbbda21b95dec612b515ccab..d0fbf8e6c650e3b377d8a6dbe1e0592583309e4a 100644
--- a/lib/spack/spack/cmd/graph.py
+++ b/lib/spack/spack/cmd/graph.py
@@ -5,7 +5,6 @@
 
 from __future__ import print_function
 
-import argparse
 import llnl.util.tty as tty
 
 import spack.cmd
@@ -38,11 +37,7 @@ def setup_parser(subparser):
         '-i', '--installed', action='store_true',
         help="graph all installed specs in dot format (implies --dot)")
 
-    arguments.add_common_arguments(subparser, ['deptype'])
-
-    subparser.add_argument(
-        'specs', nargs=argparse.REMAINDER,
-        help="specs of packages to graph")
+    arguments.add_common_arguments(subparser, ['deptype', 'specs'])
 
 
 def graph(parser, args):
diff --git a/lib/spack/spack/cmd/info.py b/lib/spack/spack/cmd/info.py
index 413b96fe180de5fd729357a7be6e9b31d9c63932..81a68dae966c0ed8beaf2238fad48d671b561335 100644
--- a/lib/spack/spack/cmd/info.py
+++ b/lib/spack/spack/cmd/info.py
@@ -11,6 +11,7 @@
 import llnl.util.tty.color as color
 from llnl.util.tty.colify import colify
 
+import spack.cmd.common.arguments as arguments
 import spack.repo
 import spack.spec
 import spack.fetch_strategy as fs
@@ -36,8 +37,7 @@ def pad(string):
 
 
 def setup_parser(subparser):
-    subparser.add_argument(
-        'name', metavar='PACKAGE', help='name of package to get info for')
+    arguments.add_common_arguments(subparser, ['package'])
 
 
 def section_title(s):
@@ -237,5 +237,5 @@ def print_text_info(pkg):
 
 
 def info(parser, args):
-    pkg = spack.repo.get(args.name)
+    pkg = spack.repo.get(args.package)
     print_text_info(pkg)
diff --git a/lib/spack/spack/cmd/install.py b/lib/spack/spack/cmd/install.py
index ace27577eb97e981d46495eca26bfcba2ffc524b..18dad6108bf84f08ec2ce80db8b472b3f47cf593 100644
--- a/lib/spack/spack/cmd/install.py
+++ b/lib/spack/spack/cmd/install.py
@@ -122,11 +122,6 @@ def setup_parser(subparser):
     cd_group = subparser.add_mutually_exclusive_group()
     arguments.add_common_arguments(cd_group, ['clean', 'dirty'])
 
-    subparser.add_argument(
-        'package',
-        nargs=argparse.REMAINDER,
-        help="spec of the package to install"
-    )
     testing = subparser.add_mutually_exclusive_group()
     testing.add_argument(
         '--test', default=None,
@@ -157,7 +152,7 @@ def setup_parser(subparser):
         help="Show usage instructions for CDash reporting"
     )
     add_cdash_args(subparser, False)
-    arguments.add_common_arguments(subparser, ['yes_to_all'])
+    arguments.add_common_arguments(subparser, ['yes_to_all', 'spec'])
 
 
 def add_cdash_args(subparser, add_help):
@@ -258,7 +253,7 @@ def install(parser, args, **kwargs):
         parser.print_help()
         return
 
-    if not args.package and not args.specfiles:
+    if not args.spec and not args.specfiles:
         # if there are no args but an active environment or spack.yaml file
         # then install the packages from it.
         env = ev.get_env(args, 'install')
@@ -293,7 +288,7 @@ def install(parser, args, **kwargs):
     if args.log_file:
         reporter.filename = args.log_file
 
-    abstract_specs = spack.cmd.parse_specs(args.package)
+    abstract_specs = spack.cmd.parse_specs(args.spec)
     tests = False
     if args.test == 'all' or args.run_tests:
         tests = True
@@ -303,7 +298,7 @@ def install(parser, args, **kwargs):
 
     try:
         specs = spack.cmd.parse_specs(
-            args.package, concretize=True, tests=tests)
+            args.spec, concretize=True, tests=tests)
     except SpackError as e:
         tty.debug(e)
         reporter.concretization_report(e.message)
diff --git a/lib/spack/spack/cmd/load.py b/lib/spack/spack/cmd/load.py
index 89dd61675eab168f9e4da8163a9afce0078b9fc5..9c48fe802ae2071cfbfda8cc7e670486dec8205b 100644
--- a/lib/spack/spack/cmd/load.py
+++ b/lib/spack/spack/cmd/load.py
@@ -3,7 +3,6 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
 from spack.cmd.common import print_module_placeholder_help, arguments
 
 description = "add package to environment using `module load`"
@@ -14,11 +13,8 @@
 def setup_parser(subparser):
     """Parser is only constructed so that this prints a nice help
        message with -h. """
-    subparser.add_argument(
-        'spec', nargs=argparse.REMAINDER,
-        help="spec of package to load with modules "
-    )
-    arguments.add_common_arguments(subparser, ['recurse_dependencies'])
+    arguments.add_common_arguments(
+        subparser, ['recurse_dependencies', 'installed_spec'])
 
 
 def load(parser, args):
diff --git a/lib/spack/spack/cmd/location.py b/lib/spack/spack/cmd/location.py
index a48ce8526119f9b9f5de9cde26fb0e4b21d9adcc..60978fe404c3bb9e9b07c8883b932d08f3fe24fe 100644
--- a/lib/spack/spack/cmd/location.py
+++ b/lib/spack/spack/cmd/location.py
@@ -6,11 +6,11 @@
 from __future__ import print_function
 
 import os
-import argparse
 import llnl.util.tty as tty
 
 import spack.environment as ev
 import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.environment
 import spack.paths
 import spack.repo
@@ -55,9 +55,7 @@ def setup_parser(subparser):
         '-e', '--env', action='store',
         help="location of an environment managed by spack")
 
-    subparser.add_argument(
-        'spec', nargs=argparse.REMAINDER,
-        help="spec of package to fetch directory for")
+    arguments.add_common_arguments(subparser, ['spec'])
 
 
 def location(parser, args):
diff --git a/lib/spack/spack/cmd/maintainers.py b/lib/spack/spack/cmd/maintainers.py
index a1fb7716f9df1410d627d4629a572ebaff85bded..a1cf477146e7196ca081137a90d19964b4601c4e 100644
--- a/lib/spack/spack/cmd/maintainers.py
+++ b/lib/spack/spack/cmd/maintainers.py
@@ -40,7 +40,7 @@ def setup_parser(subparser):
 
     # options for commands that take package arguments
     subparser.add_argument(
-        'pkg_or_user', nargs=argparse.REMAINDER,
+        'package_or_user', nargs=argparse.REMAINDER,
         help='names of packages or users to get info for')
 
 
@@ -104,31 +104,31 @@ def maintainers(parser, args):
 
     if args.all:
         if args.by_user:
-            maintainers = maintainers_to_packages(args.pkg_or_user)
+            maintainers = maintainers_to_packages(args.package_or_user)
             for user, packages in sorted(maintainers.items()):
                 color.cprint('@c{%s}: %s'
                              % (user, ', '.join(sorted(packages))))
             return 0 if maintainers else 1
 
         else:
-            packages = packages_to_maintainers(args.pkg_or_user)
+            packages = packages_to_maintainers(args.package_or_user)
             for pkg, maintainers in sorted(packages.items()):
                 color.cprint('@c{%s}: %s'
                              % (pkg, ', '.join(sorted(maintainers))))
             return 0 if packages else 1
 
     if args.by_user:
-        if not args.pkg_or_user:
+        if not args.package_or_user:
             tty.die('spack maintainers --by-user requires a user or --all')
 
-        packages = union_values(maintainers_to_packages(args.pkg_or_user))
+        packages = union_values(maintainers_to_packages(args.package_or_user))
         colify(packages)
         return 0 if packages else 1
 
     else:
-        if not args.pkg_or_user:
+        if not args.package_or_user:
             tty.die('spack maintainers requires a package or --all')
 
-        users = union_values(packages_to_maintainers(args.pkg_or_user))
+        users = union_values(packages_to_maintainers(args.package_or_user))
         colify(users)
         return 0 if users else 1
diff --git a/lib/spack/spack/cmd/mirror.py b/lib/spack/spack/cmd/mirror.py
index 10f01fd363daa85d7a92b82f634ae5ab95d9687b..520692789525054f00e247ea6d3f8fd99cf5dda1 100644
--- a/lib/spack/spack/cmd/mirror.py
+++ b/lib/spack/spack/cmd/mirror.py
@@ -5,7 +5,6 @@
 
 import sys
 
-import argparse
 import llnl.util.tty as tty
 from llnl.util.tty.colify import colify
 
@@ -39,9 +38,6 @@ def setup_parser(subparser):
     create_parser.add_argument('-d', '--directory', default=None,
                                help="directory in which to create mirror")
 
-    create_parser.add_argument(
-        'specs', nargs=argparse.REMAINDER,
-        help="specs of packages to put in mirror")
     create_parser.add_argument(
         '-a', '--all', action='store_true',
         help="mirror all versions of all packages in Spack, or all packages"
@@ -57,6 +53,7 @@ def setup_parser(subparser):
         '-n', '--versions-per-spec',
         help="the number of versions to fetch for each spec, choose 'all' to"
              " retrieve all versions of each package")
+    arguments.add_common_arguments(create_parser, ['specs'])
 
     # used to construct scope arguments below
     scopes = spack.config.scopes()
@@ -64,7 +61,8 @@ def setup_parser(subparser):
 
     # Add
     add_parser = sp.add_parser('add', help=mirror_add.__doc__)
-    add_parser.add_argument('name', help="mnemonic name for mirror")
+    add_parser.add_argument(
+        'name', help="mnemonic name for mirror", metavar="mirror")
     add_parser.add_argument(
         'url', help="url of mirror directory from 'spack mirror create'")
     add_parser.add_argument(
@@ -75,7 +73,8 @@ def setup_parser(subparser):
     # Remove
     remove_parser = sp.add_parser('remove', aliases=['rm'],
                                   help=mirror_remove.__doc__)
-    remove_parser.add_argument('name')
+    remove_parser.add_argument(
+        'name', help="mnemonic name for mirror", metavar="mirror")
     remove_parser.add_argument(
         '--scope', choices=scopes, metavar=scopes_metavar,
         default=spack.config.default_modify_scope(),
@@ -83,7 +82,8 @@ def setup_parser(subparser):
 
     # Set-Url
     set_url_parser = sp.add_parser('set-url', help=mirror_set_url.__doc__)
-    set_url_parser.add_argument('name', help="mnemonic name for mirror")
+    set_url_parser.add_argument(
+        'name', help="mnemonic name for mirror", metavar="mirror")
     set_url_parser.add_argument(
         'url', help="url of mirror directory from 'spack mirror create'")
     set_url_parser.add_argument(
diff --git a/lib/spack/spack/cmd/patch.py b/lib/spack/spack/cmd/patch.py
index 9e7cc4164b76f339917b0bad23680016051c5da5..8f91edb8f13177a98554f55b4b3b7123808a5b0d 100644
--- a/lib/spack/spack/cmd/patch.py
+++ b/lib/spack/spack/cmd/patch.py
@@ -3,8 +3,6 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
-
 import llnl.util.tty as tty
 
 import spack.repo
@@ -18,20 +16,17 @@
 
 
 def setup_parser(subparser):
-    arguments.add_common_arguments(subparser, ['no_checksum'])
-    subparser.add_argument(
-        'packages', nargs=argparse.REMAINDER,
-        help="specs of packages to stage")
+    arguments.add_common_arguments(subparser, ['no_checksum', 'specs'])
 
 
 def patch(parser, args):
-    if not args.packages:
-        tty.die("patch requires at least one package argument")
+    if not args.specs:
+        tty.die("patch requires at least one spec argument")
 
     if args.no_checksum:
         spack.config.set('config:checksum', False, scope='command_line')
 
-    specs = spack.cmd.parse_specs(args.packages, concretize=True)
+    specs = spack.cmd.parse_specs(args.specs, concretize=True)
     for spec in specs:
         package = spack.repo.get(spec)
         package.do_patch()
diff --git a/lib/spack/spack/cmd/pkg.py b/lib/spack/spack/cmd/pkg.py
index 86ff535d5ef935751866422ea8d966cbf9c17ccb..b988d6a848fbb5ce2725900fc1dcc35fffe0059b 100644
--- a/lib/spack/spack/cmd/pkg.py
+++ b/lib/spack/spack/cmd/pkg.py
@@ -6,7 +6,6 @@
 from __future__ import print_function
 
 import os
-import argparse
 import re
 
 import llnl.util.tty as tty
@@ -14,6 +13,7 @@
 from llnl.util.filesystem import working_dir
 
 import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.paths
 import spack.repo
 from spack.util.executable import which
@@ -28,8 +28,7 @@ def setup_parser(subparser):
         metavar='SUBCOMMAND', dest='pkg_command')
 
     add_parser = sp.add_parser('add', help=pkg_add.__doc__)
-    add_parser.add_argument('packages', nargs=argparse.REMAINDER,
-                            help="names of packages to add to git repo")
+    arguments.add_common_arguments(add_parser, ['packages'])
 
     list_parser = sp.add_parser('list', help=pkg_list.__doc__)
     list_parser.add_argument('rev', default='HEAD', nargs='?',
diff --git a/lib/spack/spack/cmd/remove.py b/lib/spack/spack/cmd/remove.py
index cce197af2e3b6750a78a840c7d79c96cc09a2962..049041ce8351d1b98028ee5273f21728bdd2a58b 100644
--- a/lib/spack/spack/cmd/remove.py
+++ b/lib/spack/spack/cmd/remove.py
@@ -3,11 +3,10 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
-
 import llnl.util.tty as tty
 
 import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.environment as ev
 
 
@@ -26,8 +25,7 @@ def setup_parser(subparser):
     subparser.add_argument(
         '-f', '--force', action='store_true',
         help="remove concretized spec (if any) immediately")
-    subparser.add_argument(
-        'specs', nargs=argparse.REMAINDER, help="specs to be removed")
+    arguments.add_common_arguments(subparser, ['specs'])
 
 
 def remove(parser, args):
diff --git a/lib/spack/spack/cmd/repo.py b/lib/spack/spack/cmd/repo.py
index 019813fc9f8c41c98f02f60ac3899f487da55033..83acf796a2430c283b13e67eae55de99807ca11a 100644
--- a/lib/spack/spack/cmd/repo.py
+++ b/lib/spack/spack/cmd/repo.py
@@ -51,8 +51,8 @@ def setup_parser(subparser):
     remove_parser = sp.add_parser(
         'remove', help=repo_remove.__doc__, aliases=['rm'])
     remove_parser.add_argument(
-        'path_or_namespace',
-        help="path or namespace of a Spack package repository")
+        'namespace_or_path',
+        help="namespace or path of a Spack package repository")
     remove_parser.add_argument(
         '--scope', choices=scopes, metavar=scopes_metavar,
         default=spack.config.default_modify_scope(),
@@ -101,10 +101,10 @@ def repo_add(args):
 def repo_remove(args):
     """Remove a repository from Spack's configuration."""
     repos = spack.config.get('repos', scope=args.scope)
-    path_or_namespace = args.path_or_namespace
+    namespace_or_path = args.namespace_or_path
 
     # If the argument is a path, remove that repository from config.
-    canon_path = canonicalize_path(path_or_namespace)
+    canon_path = canonicalize_path(namespace_or_path)
     for repo_path in repos:
         repo_canon_path = canonicalize_path(repo_path)
         if canon_path == repo_canon_path:
@@ -117,7 +117,7 @@ def repo_remove(args):
     for path in repos:
         try:
             repo = Repo(path)
-            if repo.namespace == path_or_namespace:
+            if repo.namespace == namespace_or_path:
                 repos.remove(path)
                 spack.config.set('repos', repos, args.scope)
                 tty.msg("Removed repository %s with namespace '%s'."
@@ -127,7 +127,7 @@ def repo_remove(args):
             continue
 
     tty.die("No repository with path or namespace: %s"
-            % path_or_namespace)
+            % namespace_or_path)
 
 
 def repo_list(args):
diff --git a/lib/spack/spack/cmd/restage.py b/lib/spack/spack/cmd/restage.py
index f74ef09a12eb5270c8b2491f64cf820e0955aa49..0f55884bfeafb1c9342404dc91f4663ad148f630 100644
--- a/lib/spack/spack/cmd/restage.py
+++ b/lib/spack/spack/cmd/restage.py
@@ -3,11 +3,10 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
-
 import llnl.util.tty as tty
 
 import spack.cmd
+import spack.cmd.common.arguments as arguments
 import spack.repo
 
 description = "revert checked out package source code"
@@ -16,15 +15,14 @@
 
 
 def setup_parser(subparser):
-    subparser.add_argument('packages', nargs=argparse.REMAINDER,
-                           help="specs of packages to restage")
+    arguments.add_common_arguments(subparser, ['specs'])
 
 
 def restage(parser, args):
-    if not args.packages:
+    if not args.specs:
         tty.die("spack restage requires at least one package spec.")
 
-    specs = spack.cmd.parse_specs(args.packages, concretize=True)
+    specs = spack.cmd.parse_specs(args.specs, concretize=True)
     for spec in specs:
         package = spack.repo.get(spec)
         package.do_restage()
diff --git a/lib/spack/spack/cmd/setup.py b/lib/spack/spack/cmd/setup.py
index 3e4f9135d7d516798bcadb0adf5354b43f9d6e46..246e3b4275658195c93fe6441be9d50f0d7651fe 100644
--- a/lib/spack/spack/cmd/setup.py
+++ b/lib/spack/spack/cmd/setup.py
@@ -30,13 +30,10 @@ def setup_parser(subparser):
     subparser.add_argument(
         '-i', '--ignore-dependencies', action='store_true', dest='ignore_deps',
         help="do not try to install dependencies of requested packages")
-    arguments.add_common_arguments(subparser, ['no_checksum'])
+    arguments.add_common_arguments(subparser, ['no_checksum', 'spec'])
     subparser.add_argument(
         '-v', '--verbose', action='store_true', dest='verbose',
         help="display verbose build output while installing")
-    subparser.add_argument(
-        'spec', nargs=argparse.REMAINDER,
-        help="specs to use for install. must contain package AND version")
 
     cd_group = subparser.add_mutually_exclusive_group()
     arguments.add_common_arguments(cd_group, ['clean', 'dirty'])
diff --git a/lib/spack/spack/cmd/spec.py b/lib/spack/spack/cmd/spec.py
index 85fe5a1a9e9bc595e2d31b4e4524402b79381793..fd03f09e575956cecf1fa6787288b7fe676f4f25 100644
--- a/lib/spack/spack/cmd/spec.py
+++ b/lib/spack/spack/cmd/spec.py
@@ -5,7 +5,6 @@
 
 from __future__ import print_function
 
-import argparse
 import contextlib
 import sys
 
@@ -47,8 +46,7 @@ def setup_parser(subparser):
     subparser.add_argument(
         '-t', '--types', action='store_true', default=False,
         help='show dependency types')
-    subparser.add_argument(
-        'specs', nargs=argparse.REMAINDER, help="specs of packages")
+    arguments.add_common_arguments(subparser, ['specs'])
 
 
 @contextlib.contextmanager
diff --git a/lib/spack/spack/cmd/stage.py b/lib/spack/spack/cmd/stage.py
index 9c0d3ad63c10c451c3286931c8be8f450837c346..1acefb723c8678c20e1ac3d7317e75e3c3a9cb43 100644
--- a/lib/spack/spack/cmd/stage.py
+++ b/lib/spack/spack/cmd/stage.py
@@ -3,8 +3,6 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
-
 import llnl.util.tty as tty
 
 import spack.environment as ev
@@ -18,14 +16,11 @@
 
 
 def setup_parser(subparser):
-    arguments.add_common_arguments(subparser, ['no_checksum'])
+    arguments.add_common_arguments(subparser, ['no_checksum', 'specs'])
     subparser.add_argument(
         '-p', '--path', dest='path',
         help="path to stage package, does not add to spack tree")
 
-    subparser.add_argument(
-        'specs', nargs=argparse.REMAINDER, help="specs of packages to stage")
-
 
 def stage(parser, args):
     if not args.specs:
diff --git a/lib/spack/spack/cmd/uninstall.py b/lib/spack/spack/cmd/uninstall.py
index 906da8d3b38725fcf34fed456b32a111cc6e0ddf..0ad42f4dfb64e77b3dd86343322e4fae747082e0 100644
--- a/lib/spack/spack/cmd/uninstall.py
+++ b/lib/spack/spack/cmd/uninstall.py
@@ -5,7 +5,6 @@
 
 from __future__ import print_function
 
-import argparse
 import sys
 
 import spack.cmd
@@ -38,17 +37,13 @@
 }
 
 
-def add_common_arguments(subparser):
+def setup_parser(subparser):
     subparser.add_argument(
         '-f', '--force', action='store_true', dest='force',
         help="remove regardless of whether other packages or environments "
         "depend on this one")
     arguments.add_common_arguments(
-        subparser, ['recurse_dependents', 'yes_to_all'])
-
-
-def setup_parser(subparser):
-    add_common_arguments(subparser)
+        subparser, ['recurse_dependents', 'yes_to_all', 'installed_specs'])
     subparser.add_argument(
         '-a', '--all', action='store_true', dest='all',
         help="USE CAREFULLY. Remove ALL installed packages that match each "
@@ -58,11 +53,6 @@ def setup_parser(subparser):
         "If used in an environment, all packages in the environment "
         "will be uninstalled.")
 
-    subparser.add_argument(
-        'packages',
-        nargs=argparse.REMAINDER,
-        help="specs of packages to uninstall")
-
 
 def find_matching_specs(env, specs, allow_multiple_matches=False, force=False):
     """Returns a list of specs matching the not necessarily
@@ -351,10 +341,10 @@ def confirm_removal(specs):
 
 
 def uninstall(parser, args):
-    if not args.packages and not args.all:
+    if not args.specs and not args.all:
         tty.die('uninstall requires at least one package argument.',
                 '  Use `spack uninstall --all` to uninstall ALL packages.')
 
     # [any] here handles the --all case by forcing all specs to be returned
-    specs = spack.cmd.parse_specs(args.packages) if args.packages else [any]
+    specs = spack.cmd.parse_specs(args.specs) if args.specs else [any]
     uninstall_specs(args, specs)
diff --git a/lib/spack/spack/cmd/unload.py b/lib/spack/spack/cmd/unload.py
index 581b37a013b70047b4fac07a5aebcbf7de1943ff..92a25478b6f6796441861554cd7ed8f8d6e936de 100644
--- a/lib/spack/spack/cmd/unload.py
+++ b/lib/spack/spack/cmd/unload.py
@@ -3,8 +3,7 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import argparse
-from spack.cmd.common import print_module_placeholder_help
+from spack.cmd.common import print_module_placeholder_help, arguments
 
 description = "remove package from environment using `module unload`"
 section = "modules"
@@ -14,9 +13,7 @@
 def setup_parser(subparser):
     """Parser is only constructed so that this prints a nice help
        message with -h. """
-    subparser.add_argument(
-        'spec', nargs=argparse.REMAINDER,
-        help='spec of package to unload with modules')
+    arguments.add_common_arguments(subparser, ['installed_spec'])
 
 
 def unload(parser, args):
diff --git a/lib/spack/spack/cmd/verify.py b/lib/spack/spack/cmd/verify.py
index 9a38284691dd29d0a507a854ab2c06fa4539aa20..b20d795ce5a43b987bad950fcc54c8c4adaca5a5 100644
--- a/lib/spack/spack/cmd/verify.py
+++ b/lib/spack/spack/cmd/verify.py
@@ -25,8 +25,8 @@ def setup_parser(subparser):
                            help="Ouptut json-formatted errors")
     subparser.add_argument('-a', '--all', action='store_true',
                            help="Verify all packages")
-    subparser.add_argument('files_or_specs', nargs=argparse.REMAINDER,
-                           help="Files or specs to verify")
+    subparser.add_argument('specs_or_files', nargs=argparse.REMAINDER,
+                           help="Specs or files to verify")
 
     type = subparser.add_mutually_exclusive_group()
     type.add_argument(
@@ -47,7 +47,7 @@ def verify(parser, args):
             setup_parser.parser.print_help()
             return 1
 
-        for file in args.files_or_specs:
+        for file in args.specs_or_files:
             results = spack.verify.check_file_manifest(file)
             if results.has_errors():
                 if args.json:
@@ -57,21 +57,21 @@ def verify(parser, args):
 
         return 0
     else:
-        spec_args = spack.cmd.parse_specs(args.files_or_specs)
+        spec_args = spack.cmd.parse_specs(args.specs_or_files)
 
     if args.all:
         query = spack.store.db.query_local if local else spack.store.db.query
 
         # construct spec list
         if spec_args:
-            spec_list = spack.cmd.parse_specs(args.files_or_specs)
+            spec_list = spack.cmd.parse_specs(args.specs_or_files)
             specs = []
             for spec in spec_list:
                 specs += query(spec, installed=True)
         else:
             specs = query(installed=True)
 
-    elif args.files_or_specs:
+    elif args.specs_or_files:
         # construct disambiguated spec list
         env = ev.get_env(args, 'verify')
         specs = list(map(lambda x: spack.cmd.disambiguate_spec(x, env,
diff --git a/lib/spack/spack/cmd/versions.py b/lib/spack/spack/cmd/versions.py
index c12e7d6290dffa011871bd17e13ba024229c4cb2..723f89ce08ef6775b1f8d7cca924c75c44726f39 100644
--- a/lib/spack/spack/cmd/versions.py
+++ b/lib/spack/spack/cmd/versions.py
@@ -5,11 +5,13 @@
 
 from __future__ import print_function
 
+import sys
+
 from llnl.util.tty.colify import colify
 import llnl.util.tty as tty
 
+import spack.cmd.common.arguments as arguments
 import spack.repo
-import sys
 
 description = "list available versions of a package"
 section = "packaging"
@@ -17,10 +19,9 @@
 
 
 def setup_parser(subparser):
-    subparser.add_argument('package', metavar='PACKAGE',
-                           help='package to list versions for')
     subparser.add_argument('-s', '--safe-only', action='store_true',
                            help='only list safe versions of the package')
+    arguments.add_common_arguments(subparser, ['package'])
 
 
 def versions(parser, args):
diff --git a/lib/spack/spack/reporters/cdash.py b/lib/spack/spack/reporters/cdash.py
index 178747706acab5d3ec9ba5af268db5063ab6bcce..580df7866fafd8832007f7a5fb2fc7394e1c7901 100644
--- a/lib/spack/spack/reporters/cdash.py
+++ b/lib/spack/spack/reporters/cdash.py
@@ -72,8 +72,8 @@ def __init__(self, args):
             tty.verbose("Using CDash auth token from environment")
             self.authtoken = os.environ.get('SPACK_CDASH_AUTH_TOKEN')
 
-        if args.package:
-            packages = args.package
+        if args.spec:
+            packages = args.spec
         else:
             packages = []
             for file in args.specfiles:
diff --git a/lib/spack/spack/test/cmd/commands.py b/lib/spack/spack/test/cmd/commands.py
index c8ec60d8239e95f0da30fdedde06a37377c0a581..2fe62b9bbaf999067e029126eb7265f18c0554c4 100644
--- a/lib/spack/spack/test/cmd/commands.py
+++ b/lib/spack/spack/test/cmd/commands.py
@@ -3,13 +3,17 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import re
-import pytest
+import filecmp
+import os
+import subprocess
 
-from llnl.util.argparsewriter import ArgparseWriter
+import pytest
 
 import spack.cmd
+from spack.cmd.commands import _positional_to_subroutine
 import spack.main
+import spack.paths
+
 
 commands = spack.main.SpackCommand('commands')
 
@@ -17,38 +21,64 @@
 spack.main.add_all_commands(parser)
 
 
-def test_commands_by_name():
+def test_names():
     """Test default output of spack commands."""
-    out = commands()
-    assert out.strip().split('\n') == sorted(spack.cmd.all_commands())
+    out1 = commands().strip().split('\n')
+    assert out1 == spack.cmd.all_commands()
+    assert 'rm' not in out1
 
+    out2 = commands('--aliases').strip().split('\n')
+    assert out1 != out2
+    assert 'rm' in out2
 
-def test_subcommands():
-    """Test subcommand traversal."""
-    out = commands('--format=subcommands')
-    assert 'spack mirror create' in out
-    assert 'spack buildcache list' in out
-    assert 'spack repo add' in out
-    assert 'spack pkg diff' in out
-    assert 'spack url parse' in out
-    assert 'spack view symlink' in out
+    out3 = commands('--format=names').strip().split('\n')
+    assert out1 == out3
 
-    class Subcommands(ArgparseWriter):
-        def begin_command(self, prog):
-            assert prog in out
 
-    Subcommands().write(parser)
+def test_subcommands():
+    """Test subcommand traversal."""
+    out1 = commands('--format=subcommands')
+    assert 'spack mirror create' in out1
+    assert 'spack buildcache list' in out1
+    assert 'spack repo add' in out1
+    assert 'spack pkg diff' in out1
+    assert 'spack url parse' in out1
+    assert 'spack view symlink' in out1
+    assert 'spack rm' not in out1
+    assert 'spack compiler add' not in out1
+
+    out2 = commands('--aliases', '--format=subcommands')
+    assert 'spack mirror create' in out2
+    assert 'spack buildcache list' in out2
+    assert 'spack repo add' in out2
+    assert 'spack pkg diff' in out2
+    assert 'spack url parse' in out2
+    assert 'spack view symlink' in out2
+    assert 'spack rm' in out2
+    assert 'spack compiler add' in out2
 
 
 def test_rst():
     """Do some simple sanity checks of the rst writer."""
-    out = commands('--format=rst')
-
-    class Subcommands(ArgparseWriter):
-        def begin_command(self, prog):
-            assert prog in out
-            assert re.sub(r' ', '-', prog) in out
-    Subcommands().write(parser)
+    out1 = commands('--format=rst')
+    assert 'spack mirror create' in out1
+    assert 'spack buildcache list' in out1
+    assert 'spack repo add' in out1
+    assert 'spack pkg diff' in out1
+    assert 'spack url parse' in out1
+    assert 'spack view symlink' in out1
+    assert 'spack rm' not in out1
+    assert 'spack compiler add' not in out1
+
+    out2 = commands('--aliases', '--format=rst')
+    assert 'spack mirror create' in out2
+    assert 'spack buildcache list' in out2
+    assert 'spack repo add' in out2
+    assert 'spack pkg diff' in out2
+    assert 'spack url parse' in out2
+    assert 'spack view symlink' in out2
+    assert 'spack rm' in out2
+    assert 'spack compiler add' in out2
 
 
 def test_rst_with_input_files(tmpdir):
@@ -109,3 +139,91 @@ def test_rst_update(tmpdir):
     assert update_file.exists()
     with update_file.open() as f:
         assert f.read() == 'empty\n'
+
+
+def test_update_with_header(tmpdir):
+    update_file = tmpdir.join('output')
+
+    # not yet created when commands is run
+    commands('--update', str(update_file))
+    assert update_file.exists()
+    with update_file.open() as f:
+        assert f.read()
+    fake_header = 'this is a header!\n\n'
+
+    filename = tmpdir.join('header.txt')
+    with filename.open('w') as f:
+        f.write(fake_header)
+
+    # created, newer than commands, but older than header
+    commands('--update', str(update_file), '--header', str(filename))
+
+    # newer than commands and header
+    commands('--update', str(update_file), '--header', str(filename))
+
+
+@pytest.mark.xfail
+def test_no_pipe_error():
+    """Make sure we don't see any pipe errors when piping output."""
+
+    proc = subprocess.Popen(
+        ['spack', 'commands', '--format=rst'],
+        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+    # Call close() on stdout to cause a broken pipe
+    proc.stdout.close()
+    proc.wait()
+    stderr = proc.stderr.read().decode('utf-8')
+
+    assert 'Broken pipe' not in stderr
+
+
+def test_bash_completion():
+    """Test the bash completion writer."""
+    out1 = commands('--format=bash')
+
+    # Make sure header not included
+    assert '_bash_completion_spack() {' not in out1
+    assert '_all_packages() {' not in out1
+
+    # Make sure subcommands appear
+    assert '_spack_remove() {' in out1
+    assert '_spack_compiler_find() {' in out1
+
+    # Make sure aliases don't appear
+    assert '_spack_rm() {' not in out1
+    assert '_spack_compiler_add() {' not in out1
+
+    # Make sure options appear
+    assert '-h --help' in out1
+
+    # Make sure subcommands are called
+    for function in _positional_to_subroutine.values():
+        assert function in out1
+
+    out2 = commands('--aliases', '--format=bash')
+
+    # Make sure aliases appear
+    assert '_spack_rm() {' in out2
+    assert '_spack_compiler_add() {' in out2
+
+
+def test_updated_completion_scripts(tmpdir):
+    """Make sure our shell tab completion scripts remain up-to-date."""
+
+    msg = ("It looks like Spack's command-line interface has been modified. "
+           "Please update Spack's shell tab completion scripts by running:\n\n"
+           "    share/spack/qa/update-completion-scripts.sh\n\n"
+           "and adding the changed files to your pull request.")
+
+    for shell in ['bash']:  # 'zsh', 'fish']:
+        header = os.path.join(
+            spack.paths.share_path, shell, 'spack-completion.in')
+        script = 'spack-completion.{0}'.format(shell)
+        old_script = os.path.join(spack.paths.share_path, script)
+        new_script = str(tmpdir.join(script))
+
+        commands('--aliases', '--format', shell,
+                 '--header', header, '--update', new_script)
+
+        assert filecmp.cmp(old_script, new_script), msg
diff --git a/lib/spack/spack/test/llnl/util/argparsewriter.py b/lib/spack/spack/test/llnl/util/argparsewriter.py
new file mode 100644
index 0000000000000000000000000000000000000000..127149bbaab30a28e8e89b38a8a811194366f87b
--- /dev/null
+++ b/lib/spack/spack/test/llnl/util/argparsewriter.py
@@ -0,0 +1,37 @@
+# Copyright 2013-2020 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+"""Tests for ``llnl/util/argparsewriter.py``
+
+These tests are fairly minimal, and ArgparseWriter is more extensively
+tested in ``cmd/commands.py``.
+"""
+
+import pytest
+
+import llnl.util.argparsewriter as aw
+
+import spack.main
+
+
+parser = spack.main.make_argument_parser()
+spack.main.add_all_commands(parser)
+
+
+def test_format_not_overridden():
+    writer = aw.ArgparseWriter('spack')
+
+    with pytest.raises(NotImplementedError):
+        writer.write(parser)
+
+
+def test_completion_format_not_overridden():
+    writer = aw.ArgparseCompletionWriter('spack')
+
+    assert writer.positionals([]) == ''
+    assert writer.optionals([]) == ''
+    assert writer.subcommands([]) == ''
+
+    writer.write(parser)
diff --git a/share/spack/bash/spack-completion.in b/share/spack/bash/spack-completion.in
new file mode 100755
index 0000000000000000000000000000000000000000..2ab39a57a383837584e9cc48acfa3d7fa7ac3f72
--- /dev/null
+++ b/share/spack/bash/spack-completion.in
@@ -0,0 +1,309 @@
+# Copyright 2013-2020 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+
+# NOTE: spack-completion.bash is auto-generated by:
+#
+#   $ spack commands --aliases --format=bash
+#       --header=bash/spack-completion.in --update=spack-completion.bash
+#
+# Please do not manually modify this file.
+
+
+# The following global variables are set by Bash programmable completion:
+#
+#     COMP_CWORD:      An index into ${COMP_WORDS} of the word containing the
+#                      current cursor position
+#     COMP_KEY:        The key (or final key of a key sequence) used to invoke
+#                      the current completion function
+#     COMP_LINE:       The current command line
+#     COMP_POINT:      The index of the current cursor position relative to the
+#                      beginning of the current command
+#     COMP_TYPE:       Set to an integer value corresponding to the type of
+#                      completion attempted that caused a completion function
+#                      to be called
+#     COMP_WORDBREAKS: The set of characters that the readline library treats
+#                      as word separators when performing word completion
+#     COMP_WORDS:      An array variable consisting of the individual words in
+#                      the current command line
+#
+# The following global variable is used by Bash programmable completion:
+#
+#     COMPREPLY:       An array variable from which bash reads the possible
+#                      completions generated by a shell function invoked by the
+#                      programmable completion facility
+#
+# See `man bash` for more details.
+
+# Bash programmable completion for Spack
+_bash_completion_spack() {
+    # In all following examples, let the cursor be denoted by brackets, i.e. []
+
+    # For our purposes, flags should not affect tab completion. For instance,
+    # `spack install []` and `spack -d install --jobs 8 []` should both give the same
+    # possible completions. Therefore, we need to ignore any flags in COMP_WORDS.
+    local COMP_WORDS_NO_FLAGS=()
+    local index=0
+    while [[ "$index" -lt "$COMP_CWORD" ]]
+    do
+        if [[ "${COMP_WORDS[$index]}" == [a-z]* ]]
+        then
+            COMP_WORDS_NO_FLAGS+=("${COMP_WORDS[$index]}")
+        fi
+        let index++
+    done
+
+    # Options will be listed by a subfunction named after non-flag arguments.
+    # For example, `spack -d install []` will call _spack_install
+    # and `spack compiler add []` will call _spack_compiler_add
+    local subfunction=$(IFS='_'; echo "_${COMP_WORDS_NO_FLAGS[*]}")
+
+    # Translate dashes to underscores, as dashes are not permitted in
+    # compatibility mode. See https://github.com/spack/spack/pull/4079
+    subfunction=${subfunction//-/_}
+
+    # However, the word containing the current cursor position needs to be
+    # added regardless of whether or not it is a flag. This allows us to
+    # complete something like `spack install --keep-st[]`
+    COMP_WORDS_NO_FLAGS+=("${COMP_WORDS[$COMP_CWORD]}")
+
+    # Since we have removed all words after COMP_CWORD, we can safely assume
+    # that COMP_CWORD_NO_FLAGS is simply the index of the last element
+    local COMP_CWORD_NO_FLAGS=$((${#COMP_WORDS_NO_FLAGS[@]} - 1))
+
+    # There is no guarantee that the cursor is at the end of the command line
+    # when tab completion is envoked. For example, in the following situation:
+    #     `spack -d [] install`
+    # if the user presses the TAB key, a list of valid flags should be listed.
+    # Note that we cannot simply ignore everything after the cursor. In the
+    # previous scenario, the user should expect to see a list of flags, but
+    # not of other subcommands. Obviously, `spack -d list install` would be
+    # invalid syntax. To accomplish this, we use the variable list_options
+    # which is true if the current word starts with '-' or if the cursor is
+    # not at the end of the line.
+    local list_options=false
+    if [[ "${COMP_WORDS[$COMP_CWORD]}" == -* || "$COMP_POINT" -ne "${#COMP_LINE}" ]]
+    then
+        list_options=true
+    fi
+
+    # In general, when envoking tab completion, the user is not expecting to
+    # see optional flags mixed in with subcommands or package names. Tab
+    # completion is used by those who are either lazy or just bad at spelling.
+    # If someone doesn't remember what flag to use, seeing single letter flags
+    # in their results won't help them, and they should instead consult the
+    # documentation. However, if the user explicitly declares that they are
+    # looking for a flag, we can certainly help them out.
+    #     `spack install -[]`
+    # and
+    #     `spack install --[]`
+    # should list all flags and long flags, respectively. Furthermore, if a
+    # subcommand has no non-flag completions, such as `spack arch []`, it
+    # should list flag completions.
+
+    local cur=${COMP_WORDS_NO_FLAGS[$COMP_CWORD_NO_FLAGS]}
+
+    # If the cursor is in the middle of the line, like:
+    #     `spack -d [] install`
+    # COMP_WORDS will not contain the empty character, so we have to add it.
+    if [[ "${COMP_LINE:$COMP_POINT:1}" == " " ]]
+    then
+        cur=""
+    fi
+
+    # Uncomment this line to enable logging
+    #_test_vars >> temp
+
+    # Make sure function exists before calling it
+    if [[ "$(type -t $subfunction)" == "function" ]]
+    then
+        $subfunction
+        COMPREPLY=($(compgen -W "$SPACK_COMPREPLY" -- "$cur"))
+    fi
+}
+
+# Helper functions for subcommands
+# Results of each query are cached via environment variables
+
+_subcommands() {
+    if [[ -z "${SPACK_SUBCOMMANDS:-}" ]]
+    then
+        SPACK_SUBCOMMANDS="$(spack commands)"
+    fi
+    SPACK_COMPREPLY="$SPACK_SUBCOMMANDS"
+}
+
+_all_packages() {
+    if [[ -z "${SPACK_ALL_PACKAGES:-}" ]]
+    then
+        SPACK_ALL_PACKAGES="$(spack list)"
+    fi
+    SPACK_COMPREPLY="$SPACK_ALL_PACKAGES"
+}
+
+_all_resource_hashes() {
+    if [[ -z "${SPACK_ALL_RESOURCES_HASHES:-}" ]]
+    then
+        SPACK_ALL_RESOURCE_HASHES="$(spack resource list --only-hashes)"
+    fi
+    SPACK_COMPREPLY="$SPACK_ALL_RESOURCE_HASHES"
+}
+
+_installed_packages() {
+    if [[ -z "${SPACK_INSTALLED_PACKAGES:-}" ]]
+    then
+        SPACK_INSTALLED_PACKAGES="$(spack --color=never find --no-groups)"
+    fi
+    SPACK_COMPREPLY="$SPACK_INSTALLED_PACKAGES"
+}
+
+_installed_compilers() {
+    if [[ -z "${SPACK_INSTALLED_COMPILERS:-}" ]]
+    then
+        SPACK_INSTALLED_COMPILERS="$(spack compilers | egrep -v "^(-|=)")"
+    fi
+    SPACK_COMPREPLY="$SPACK_INSTALLED_COMPILERS"
+}
+
+_providers() {
+    if [[ -z "${SPACK_PROVIDERS:-}" ]]
+    then
+        SPACK_PROVIDERS="$(spack providers)"
+    fi
+    SPACK_COMPREPLY="$SPACK_PROVIDERS"
+}
+
+_mirrors() {
+    if [[ -z "${SPACK_MIRRORS:-}" ]]
+    then
+        SPACK_MIRRORS="$(spack mirror list | awk '{print $1}')"
+    fi
+    SPACK_COMPREPLY="$SPACK_MIRRORS"
+}
+
+_repos() {
+    if [[ -z "${SPACK_REPOS:-}" ]]
+    then
+        SPACK_REPOS="$(spack repo list | awk '{print $1}')"
+    fi
+    SPACK_COMPREPLY="$SPACK_REPOS"
+}
+
+_tests() {
+    if [[ -z "${SPACK_TESTS:-}" ]]
+    then
+        SPACK_TESTS="$(spack test -l)"
+    fi
+    SPACK_COMPREPLY="$SPACK_TESTS"
+}
+
+_environments() {
+    if [[ -z "${SPACK_ENVIRONMENTS:-}" ]]
+    then
+        SPACK_ENVIRONMENTS="$(spack env list)"
+    fi
+    SPACK_COMPREPLY="$SPACK_ENVIRONMENTS"
+}
+
+_keys() {
+    if [[ -z "${SPACK_KEYS:-}" ]]
+    then
+        SPACK_KEYS="$(spack gpg list)"
+    fi
+    SPACK_COMPREPLY="$SPACK_KEYS"
+}
+
+_config_sections() {
+    if [[ -z "${SPACK_CONFIG_SECTIONS:-}" ]]
+    then
+        SPACK_CONFIG_SECTIONS="compilers mirrors repos packages modules config upstreams"
+    fi
+    SPACK_COMPREPLY="$SPACK_CONFIG_SECTIONS"
+}
+
+_extensions() {
+    if [[ -z "${SPACK_EXTENSIONS:-}" ]]
+    then
+        SPACK_EXTENSIONS="aspell go-bootstrap go icedtea jdk kim-api lua matlab mofem-cephas octave openjdk perl python r ruby rust tcl yorick"
+    fi
+    SPACK_COMPREPLY="$SPACK_EXTENSIONS"
+}
+
+# Testing functions
+
+# Function for unit testing tab completion
+# Syntax: _spack_completions spack install py-
+_spack_completions() {
+    local COMP_CWORD COMP_KEY COMP_LINE COMP_POINT COMP_TYPE COMP_WORDS COMPREPLY
+
+    # Set each variable the way bash would
+    COMP_LINE="$*"
+    COMP_POINT=${#COMP_LINE}
+    COMP_WORDS=("$@")
+    if [[ ${COMP_LINE: -1} == ' ' ]]
+    then
+        COMP_WORDS+=('')
+    fi
+    COMP_CWORD=$((${#COMP_WORDS[@]} - 1))
+    COMP_KEY=9    # ASCII 09: Horizontal Tab
+    COMP_TYPE=64  # ASCII 64: '@', to list completions if the word is not unmodified
+
+    # Run Spack's tab completion function
+    _bash_completion_spack
+
+    # Return the result
+    echo "${COMPREPLY[@]:-}"
+}
+
+# Log the environment variables used
+# Syntax: _test_vars >> temp
+_test_vars() {
+    echo "-----------------------------------------------------"
+    echo "Variables set by bash:"
+    echo
+    echo "COMP_LINE:                '$COMP_LINE'"
+    echo "# COMP_LINE:              '${#COMP_LINE}'"
+    echo "COMP_WORDS:               $(_pretty_print COMP_WORDS[@])"
+    echo "# COMP_WORDS:             '${#COMP_WORDS[@]}'"
+    echo "COMP_CWORD:               '$COMP_CWORD'"
+    echo "COMP_KEY:                 '$COMP_KEY'"
+    echo "COMP_POINT:               '$COMP_POINT'"
+    echo "COMP_TYPE:                '$COMP_TYPE'"
+    echo "COMP_WORDBREAKS:          '$COMP_WORDBREAKS'"
+    echo
+    echo "Intermediate variables:"
+    echo
+    echo "COMP_WORDS_NO_FLAGS:      $(_pretty_print COMP_WORDS_NO_FLAGS[@])"
+    echo "# COMP_WORDS_NO_FLAGS:    '${#COMP_WORDS_NO_FLAGS[@]}'"
+    echo "COMP_CWORD_NO_FLAGS:      '$COMP_CWORD_NO_FLAGS'"
+    echo
+    echo "Subfunction:              '$subfunction'"
+    if $list_options
+    then
+        echo "List options:             'True'"
+    else
+        echo "List options:             'False'"
+    fi
+    echo "Current word:             '$cur'"
+}
+
+# Pretty-prints one or more arrays
+# Syntax: _pretty_print array1[@] ...
+_pretty_print() {
+    for arg in $@
+    do
+        local array=("${!arg}")
+        printf "$arg: ["
+        printf   "'%s'" "${array[0]}"
+        printf ", '%s'" "${array[@]:1}"
+        echo "]"
+    done
+}
+
+complete -o bashdefault -o default -F _bash_completion_spack spack
+
+# Spack commands
+#
+# Everything below here is auto-generated.
diff --git a/share/spack/qa/completion-test.sh b/share/spack/qa/completion-test.sh
new file mode 100755
index 0000000000000000000000000000000000000000..5b326b4a6d794cb275ca7b6591e18dd75edfb135
--- /dev/null
+++ b/share/spack/qa/completion-test.sh
@@ -0,0 +1,89 @@
+#!/bin/sh
+#
+# Copyright 2013-2020 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+#
+# This script tests that Spack's tab completion scripts work.
+#
+# The tests are portable to bash, zsh, and bourne shell, and can be run
+# in any of these shells.
+#
+
+export QA_DIR=$(dirname "$0")
+export SHARE_DIR=$(cd "$QA_DIR/.." && pwd)
+export SPACK_ROOT=$(cd "$QA_DIR/../../.." && pwd)
+
+. "$QA_DIR/test-framework.sh"
+
+# Fail on undefined variables
+set -u
+
+# Source setup-env.sh before tests
+. "$SHARE_DIR/setup-env.sh"
+. "$SHARE_DIR/spack-completion.$_sp_shell"
+
+title "Testing spack-completion.$_sp_shell with $_sp_shell"
+
+# Spack command is now available
+succeeds which spack
+
+title 'Testing all subcommands'
+while IFS= read -r line
+do
+    # Test that completion with no args works
+    succeeds _spack_completions ${line[*]} ''
+
+    # Test that completion with flags works
+    contains '-h --help' _spack_completions ${line[*]} -
+done <<- EOF
+    $(spack commands --aliases --format=subcommands)
+EOF
+
+title 'Testing for correct output'
+contains 'compiler' _spack_completions spack ''
+contains 'install' _spack_completions spack inst
+contains 'find' _spack_completions spack help ''
+contains 'hdf5' _spack_completions spack list ''
+contains 'py-numpy' _spack_completions spack list py-
+contains 'mpi' _spack_completions spack providers ''
+contains 'builtin' _spack_completions spack repo remove ''
+contains 'packages' _spack_completions spack config edit ''
+contains 'python' _spack_completions spack extensions ''
+contains 'hdf5' _spack_completions spack -d install --jobs 8 ''
+contains 'hdf5' _spack_completions spack install -v ''
+
+# XFAIL: Fails for Python 2.6 because pkg_resources not found?
+#contains 'compilers.py' _spack_completions spack test ''
+
+title 'Testing debugging functions'
+
+# This is a particularly tricky case that involves the following situation:
+#     `spack -d [] install `
+# Here, [] represents the cursor, which is in the middle of the line.
+# We should tab-complete optional flags for `spack`, not optional flags for
+# `spack install` or package names.
+COMP_LINE='spack -d  install '
+COMP_POINT=9
+COMP_WORDS=(spack -d install)
+COMP_CWORD=2
+COMP_KEY=9
+COMP_TYPE=64
+
+_bash_completion_spack
+contains "--all-help" echo "${COMPREPLY[@]}"
+
+contains "['spack', '-d', 'install', '']" _pretty_print COMP_WORDS[@]
+
+# Set the rest of the intermediate variables manually
+COMP_WORDS_NO_FLAGS=(spack install)
+COMP_CWORD_NO_FLAGS=1
+subfunction=_spack
+cur=
+
+list_options=true
+contains "'True'" _test_vars
+list_options=false
+contains "'False'" _test_vars
diff --git a/share/spack/qa/run-unit-tests b/share/spack/qa/run-unit-tests
index 8ba6eed350064de7a0d19bef80a1628be739ddf3..52748dacdf68c0f85850cfcd5d60a796a9a50cd5 100755
--- a/share/spack/qa/run-unit-tests
+++ b/share/spack/qa/run-unit-tests
@@ -23,7 +23,7 @@
 ORIGINAL_PATH="$PATH"
 
 . "$(dirname $0)/setup.sh"
-check_dependencies ${coverage} git hg svn
+check_dependencies $coverage git hg svn
 
 # Move to root directory of Spack
 # Allows script to be run from anywhere
@@ -46,7 +46,7 @@ extra_args=""
 if [[ -n "$@" ]]; then
     extra_args="-k $@"
 fi
-${coverage_run} bin/spack test -x --verbose "$extra_args"
+$coverage_run bin/spack test -x --verbose "$extra_args"
 
 #-----------------------------------------------------------
 # Run tests for setup-env.sh
@@ -57,15 +57,18 @@ export PATH="$ORIGINAL_PATH"
 unset spack
 
 # start in the spack root directory
-cd $SPACK_ROOT
+cd "$SPACK_ROOT"
 
 # Run bash tests with coverage enabled, but pipe output to /dev/null
 # because it seems that kcov seems to undo the script's redirection
 if [ "$BASH_COVERAGE" = true ]; then
-    ${QA_DIR}/bashcov ${QA_DIR}/setup-env-test.sh &> /dev/null
+    "$QA_DIR/bashcov" "$QA_DIR/setup-env-test.sh" &> /dev/null
+    "$QA_DIR/bashcov" "$QA_DIR/completion-test.sh" &> /dev/null
 fi
 
 # run the test scripts for their output (these will print nicely)
-bash ${QA_DIR}/setup-env-test.sh
-zsh  ${QA_DIR}/setup-env-test.sh
-dash ${QA_DIR}/setup-env-test.sh
+bash "$QA_DIR/setup-env-test.sh"
+zsh  "$QA_DIR/setup-env-test.sh"
+dash "$QA_DIR/setup-env-test.sh"
+
+bash "$QA_DIR/completion-test.sh"
diff --git a/share/spack/qa/setup-env-test.sh b/share/spack/qa/setup-env-test.sh
index 7613637984d68982561a79a18dea1eba9afe9dd0..66284d1a96c749e8cac197b749477251dd825ea5 100755
--- a/share/spack/qa/setup-env-test.sh
+++ b/share/spack/qa/setup-env-test.sh
@@ -12,159 +12,11 @@
 # in any of these shells.
 #
 
-# ------------------------------------------------------------------------
-# Functions for color output.
-# ------------------------------------------------------------------------
-
-# Colors for output
-red='\033[1;31m'
-cyan='\033[1;36m'
-green='\033[1;32m'
-reset='\033[0m'
-
-echo_red() {
-    printf "${red}$*${reset}\n"
-}
-
-echo_green() {
-    printf "${green}$*${reset}\n"
-}
-
-echo_msg() {
-    printf "${cyan}$*${reset}\n"
-}
-
-# ------------------------------------------------------------------------
-# Generic functions for testing shell code.
-# ------------------------------------------------------------------------
-
-# counts of test successes and failures.
-success=0
-errors=0
-
-# Print out a header for a group of tests.
-title() {
-    echo
-    echo_msg "$@"
-    echo_msg "---------------------------------"
-}
-
-# echo FAIL in red text; increment failures
-fail() {
-    echo_red FAIL
-    errors=$((errors+1))
-}
-
-#
-# Echo SUCCESS in green; increment successes
-#
-pass() {
-    echo_green SUCCESS
-    success=$((success+1))
-}
-
-#
-# Run a command and suppress output unless it fails.
-# On failure, echo the exit code and output.
-#
-succeeds() {
-    printf "'%s' succeeds ... " "$*"
-    output=$($* 2>&1)
-    err="$?"
-
-    if [ "$err" != 0 ]; then
-        fail
-        echo_red "Command failed with error $err."
-        if [ -n "$output" ]; then
-            echo_msg "Output:"
-            echo "$output"
-        else
-            echo_msg "No output."
-        fi
-    else
-        pass
-    fi
-}
-
-#
-# Run a command and suppress output unless it succeeds.
-# If the command succeeds, echo the output.
-#
-fails() {
-    printf "'%s' fails ... " "$*"
-    output=$("$@" 2>&1)
-    err="$?"
-
-    if [ "$err" = 0 ]; then
-        fail
-        echo_red "Command failed with error $err."
-        if [ -n "$output" ]; then
-            echo_msg "Output:"
-            echo "$output"
-        else
-            echo_msg "No output."
-        fi
-    else
-        pass
-    fi
-}
-
-#
-# Ensure that a string is in the output of a command.
-# Suppresses output on success.
-# On failure, echo the exit code and output.
-#
-contains() {
-    string="$1"
-    shift
-
-    printf "'%s' output contains '$string' ... " "$*"
-    output=$("$@" 2>&1)
-    err="$?"
-
-    if [ "${output#*$string}" = "${output}" ]; then
-        fail
-        echo_red "Command exited with error $err."
-        echo_red "'$string' was not in output."
-        if [ -n "$output" ]; then
-            echo_msg "Output:"
-            echo "$output"
-        else
-            echo_msg "No output."
-        fi
-    else
-        pass
-    fi
-}
-
-#
-# Ensure that a variable is set.
-#
-is_set() {
-    printf "'%s' is set ... " "$1"
-    if eval "[ -z \${${1:-}+x} ]"; then
-        fail
-        echo_msg "$1 was not set!"
-    else
-        pass
-    fi
-}
-
-#
-# Ensure that a variable is not set.
-# Fails and prints the value of the variable if it is set.
-#
-is_not_set() {
-    printf "'%s' is not set ... " "$1"
-    if eval "[ ! -z \${${1:-}+x} ]"; then
-        fail
-        echo_msg "$1 was set:"
-        echo "    $1"
-    else
-        pass
-    fi
-}
+export QA_DIR=$(dirname "$0")
+export SHARE_DIR=$(cd "$QA_DIR/.." && pwd)
+export SPACK_ROOT=$(cd "$QA_DIR/../../.." && pwd)
 
+. "$QA_DIR/test-framework.sh"
 
 # -----------------------------------------------------------------------
 # Instead of invoking the module commands, we print the
@@ -184,28 +36,28 @@ module() {
 # Make sure no environment is active
 unset SPACK_ENV
 
-# fail on undefined variables
+# Fail on undefined variables
 set -u
 
 # Source setup-env.sh before tests
-.  share/spack/setup-env.sh
+. "$SHARE_DIR/setup-env.sh"
 
-# bash should expand aliases even when non-interactive
+# Bash should expand aliases even when non-interactive
 if [ -n "${BASH:-}" ]; then
     shopt -s expand_aliases
 fi
 
 title "Testing setup-env.sh with $_sp_shell"
 
-# spack command is now avaialble
+# Spack command is now available
 succeeds which spack
 
-# mock cd command (intentionally define only AFTER setup-env.sh)
+# Mock cd command (intentionally define only AFTER setup-env.sh)
 cd() {
     echo cd "$@"
 }
 
-# create a fake mock package install and store its location for later
+# Create a fake mock package install and store its location for later
 title "Setup"
 echo "Creating a mock package installation"
 spack -m install --fake a
@@ -215,19 +67,13 @@ a_module=$(spack -m module tcl find a)
 b_install=$(spack location -i b)
 b_module=$(spack -m module tcl find b)
 
-# create a test environment for tesitng environment commands
+# Create a test environment for testing environment commands
 echo "Creating a mock environment"
 spack env create spack_test_env
 test_env_location=$(spack location -e spack_test_env)
 
-# ensure that we uninstall b on exit
+# Ensure that we uninstall b on exit
 cleanup() {
-    if [ "$?" != 0 ]; then
-        trapped_error=true
-    else
-        trapped_error=false
-    fi
-
     echo "Removing test environment before exiting."
     spack env deactivate 2>&1 > /dev/null
     spack env rm -y spack_test_env
@@ -235,24 +81,7 @@ cleanup() {
     title "Cleanup"
     echo "Removing test packages before exiting."
     spack -m uninstall -yf b a
-
-    echo
-    echo "$success tests succeeded."
-    echo "$errors tests failed."
-
-    if [ "$trapped_error" = true ]; then
-        echo "Exited due to an error."
-    fi
-
-    if [ "$errors" = 0 ] && [ "$trapped_error" = false ]; then
-        pass
-        exit 0
-    else
-        fail
-        exit 1
-    fi
 }
-trap cleanup EXIT
 
 # -----------------------------------------------------------------------
 # Test all spack commands with special env support
diff --git a/share/spack/qa/test-framework.sh b/share/spack/qa/test-framework.sh
new file mode 100755
index 0000000000000000000000000000000000000000..14b58bbecfea9f790d88577d663eb59d0b606f93
--- /dev/null
+++ b/share/spack/qa/test-framework.sh
@@ -0,0 +1,195 @@
+# Copyright 2013-2020 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+#
+# A testing framework for any POSIX-compatible shell.
+#
+
+# ------------------------------------------------------------------------
+# Functions for color output.
+# ------------------------------------------------------------------------
+
+# Colors for output
+red='\033[1;31m'
+cyan='\033[1;36m'
+green='\033[1;32m'
+reset='\033[0m'
+
+echo_red() {
+    printf "${red}$*${reset}\n"
+}
+
+echo_green() {
+    printf "${green}$*${reset}\n"
+}
+
+echo_msg() {
+    printf "${cyan}$*${reset}\n"
+}
+
+# ------------------------------------------------------------------------
+# Generic functions for testing shell code.
+# ------------------------------------------------------------------------
+
+# counts of test successes and failures.
+success=0
+errors=0
+
+# Print out a header for a group of tests.
+title() {
+    echo
+    echo_msg "$@"
+    echo_msg "---------------------------------"
+}
+
+# echo FAIL in red text; increment failures
+fail() {
+    echo_red FAIL
+    errors=$((errors+1))
+}
+
+#
+# Echo SUCCESS in green; increment successes
+#
+pass() {
+    echo_green SUCCESS
+    success=$((success+1))
+}
+
+#
+# Run a command and suppress output unless it fails.
+# On failure, echo the exit code and output.
+#
+succeeds() {
+    printf "'%s' succeeds ... " "$*"
+    output=$("$@" 2>&1)
+    err="$?"
+
+    if [ "$err" != 0 ]; then
+        fail
+        echo_red "Command failed with error $err."
+        if [ -n "$output" ]; then
+            echo_msg "Output:"
+            echo "$output"
+        else
+            echo_msg "No output."
+        fi
+    else
+        pass
+    fi
+}
+
+#
+# Run a command and suppress output unless it succeeds.
+# If the command succeeds, echo the output.
+#
+fails() {
+    printf "'%s' fails ... " "$*"
+    output=$("$@" 2>&1)
+    err="$?"
+
+    if [ "$err" = 0 ]; then
+        fail
+        echo_red "Command failed with error $err."
+        if [ -n "$output" ]; then
+            echo_msg "Output:"
+            echo "$output"
+        else
+            echo_msg "No output."
+        fi
+    else
+        pass
+    fi
+}
+
+#
+# Ensure that a string is in the output of a command.
+# Suppresses output on success.
+# On failure, echo the exit code and output.
+#
+contains() {
+    string="$1"
+    shift
+
+    printf "'%s' output contains '$string' ... " "$*"
+    output=$("$@" 2>&1)
+    err="$?"
+
+    if [ "${output#*$string}" = "${output}" ]; then
+        fail
+        echo_red "Command exited with error $err."
+        echo_red "'$string' was not in output."
+        if [ -n "$output" ]; then
+            echo_msg "Output:"
+            echo "$output"
+        else
+            echo_msg "No output."
+        fi
+    else
+        pass
+    fi
+}
+
+#
+# Ensure that a variable is set.
+#
+is_set() {
+    printf "'%s' is set ... " "$1"
+    if eval "[ -z \${${1:-}+x} ]"; then
+        fail
+        echo_msg "$1 was not set!"
+    else
+        pass
+    fi
+}
+
+#
+# Ensure that a variable is not set.
+# Fails and prints the value of the variable if it is set.
+#
+is_not_set() {
+    printf "'%s' is not set ... " "$1"
+    if eval "[ ! -z \${${1:-}+x} ]"; then
+        fail
+        echo_msg "$1 was set:"
+        echo "    $1"
+    else
+        pass
+    fi
+}
+
+#
+# Report the number of tests that succeeded and failed on exit.
+#
+teardown() {
+    if [ "$?" != 0 ]; then
+        trapped_error=true
+    else
+        trapped_error=false
+    fi
+
+    if type cleanup &> /dev/null
+    then
+        cleanup
+    fi
+
+    echo
+    echo "$success tests succeeded."
+    echo "$errors tests failed."
+
+    if [ "$trapped_error" = true ]; then
+        echo "Exited due to an error."
+    fi
+
+    if [ "$errors" = 0 ] && [ "$trapped_error" = false ]; then
+        pass
+        exit 0
+    else
+        fail
+        exit 1
+    fi
+}
+
+trap teardown EXIT
diff --git a/share/spack/qa/update-completion-scripts.sh b/share/spack/qa/update-completion-scripts.sh
new file mode 100755
index 0000000000000000000000000000000000000000..8fcd32145768dd152022d4897478601cb46cd3a9
--- /dev/null
+++ b/share/spack/qa/update-completion-scripts.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+#
+# Copyright 2013-2020 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+# Updates Spack's shell tab completion scripts
+
+# Switch to parent directory
+QA_DIR="$(dirname "${BASH_SOURCE[0]}")"
+cd "$QA_DIR/.."
+
+# Update each shell
+for shell in bash # zsh fish
+do
+    header=$shell/spack-completion.in
+    script=spack-completion.$shell
+
+    rm -f $script
+    spack commands --aliases --format=$shell --header=$header --update=$script
+    chmod +x $script
+done
diff --git a/share/spack/spack-completion.bash b/share/spack/spack-completion.bash
index a8dcbff5c0777fd18e8fecbcf50afd0c268aed44..0284e81113167d67d5692cca57f209fbcc6f99c1 100755
--- a/share/spack/spack-completion.bash
+++ b/share/spack/spack-completion.bash
@@ -4,16 +4,41 @@
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
 
-# The following global variables are used/set by Bash programmable completion
-#     COMP_CWORD: An index into ${COMP_WORDS} of the word containing the
-#                 current cursor position
-#     COMP_LINE:  The current command line
-#     COMP_WORDS: an array containing individual command arguments typed so far
-#     COMPREPLY:  an array containing possible completions as a result of your
-#                 function
+# NOTE: spack-completion.bash is auto-generated by:
+#
+#   $ spack commands --aliases --format=bash
+#       --header=bash/spack-completion.in --update=spack-completion.bash
+#
+# Please do not manually modify this file.
+
+
+# The following global variables are set by Bash programmable completion:
+#
+#     COMP_CWORD:      An index into ${COMP_WORDS} of the word containing the
+#                      current cursor position
+#     COMP_KEY:        The key (or final key of a key sequence) used to invoke
+#                      the current completion function
+#     COMP_LINE:       The current command line
+#     COMP_POINT:      The index of the current cursor position relative to the
+#                      beginning of the current command
+#     COMP_TYPE:       Set to an integer value corresponding to the type of
+#                      completion attempted that caused a completion function
+#                      to be called
+#     COMP_WORDBREAKS: The set of characters that the readline library treats
+#                      as word separators when performing word completion
+#     COMP_WORDS:      An array variable consisting of the individual words in
+#                      the current command line
+#
+# The following global variable is used by Bash programmable completion:
+#
+#     COMPREPLY:       An array variable from which bash reads the possible
+#                      completions generated by a shell function invoked by the
+#                      programmable completion facility
+#
+# See `man bash` for more details.
 
 # Bash programmable completion for Spack
-_bash_completion_spack () {
+_bash_completion_spack() {
     # In all following examples, let the cursor be denoted by brackets, i.e. []
 
     # For our purposes, flags should not affect tab completion. For instance,
@@ -46,7 +71,7 @@ _bash_completion_spack () {
 
     # Since we have removed all words after COMP_CWORD, we can safely assume
     # that COMP_CWORD_NO_FLAGS is simply the index of the last element
-    local COMP_CWORD_NO_FLAGS=$(( ${#COMP_WORDS_NO_FLAGS[@]} - 1 ))
+    local COMP_CWORD_NO_FLAGS=$((${#COMP_WORDS_NO_FLAGS[@]} - 1))
 
     # There is no guarantee that the cursor is at the end of the command line
     # when tab completion is envoked. For example, in the following situation:
@@ -59,8 +84,7 @@ _bash_completion_spack () {
     # which is true if the current word starts with '-' or if the cursor is
     # not at the end of the line.
     local list_options=false
-    if [[ "${COMP_WORDS[$COMP_CWORD]}" == -* || \
-          "$COMP_CWORD" -ne "${#COMP_WORDS[@]}-1" ]]
+    if [[ "${COMP_WORDS[$COMP_CWORD]}" == -* || "$COMP_POINT" -ne "${#COMP_LINE}" ]]
     then
         list_options=true
     fi
@@ -80,1266 +104,1488 @@ _bash_completion_spack () {
     # should list flag completions.
 
     local cur=${COMP_WORDS_NO_FLAGS[$COMP_CWORD_NO_FLAGS]}
-    local prev=${COMP_WORDS_NO_FLAGS[$COMP_CWORD_NO_FLAGS-1]}
 
-    #_test_vars
+    # If the cursor is in the middle of the line, like:
+    #     `spack -d [] install`
+    # COMP_WORDS will not contain the empty character, so we have to add it.
+    if [[ "${COMP_LINE:$COMP_POINT:1}" == " " ]]
+    then
+        cur=""
+    fi
+
+    # Uncomment this line to enable logging
+    #_test_vars >> temp
 
     # Make sure function exists before calling it
     if [[ "$(type -t $subfunction)" == "function" ]]
     then
-        COMPREPLY=($($subfunction))
+        $subfunction
+        COMPREPLY=($(compgen -W "$SPACK_COMPREPLY" -- "$cur"))
+    fi
+}
+
+# Helper functions for subcommands
+# Results of each query are cached via environment variables
+
+_subcommands() {
+    if [[ -z "${SPACK_SUBCOMMANDS:-}" ]]
+    then
+        SPACK_SUBCOMMANDS="$(spack commands)"
+    fi
+    SPACK_COMPREPLY="$SPACK_SUBCOMMANDS"
+}
+
+_all_packages() {
+    if [[ -z "${SPACK_ALL_PACKAGES:-}" ]]
+    then
+        SPACK_ALL_PACKAGES="$(spack list)"
+    fi
+    SPACK_COMPREPLY="$SPACK_ALL_PACKAGES"
+}
+
+_all_resource_hashes() {
+    if [[ -z "${SPACK_ALL_RESOURCES_HASHES:-}" ]]
+    then
+        SPACK_ALL_RESOURCE_HASHES="$(spack resource list --only-hashes)"
+    fi
+    SPACK_COMPREPLY="$SPACK_ALL_RESOURCE_HASHES"
+}
+
+_installed_packages() {
+    if [[ -z "${SPACK_INSTALLED_PACKAGES:-}" ]]
+    then
+        SPACK_INSTALLED_PACKAGES="$(spack --color=never find --no-groups)"
+    fi
+    SPACK_COMPREPLY="$SPACK_INSTALLED_PACKAGES"
+}
+
+_installed_compilers() {
+    if [[ -z "${SPACK_INSTALLED_COMPILERS:-}" ]]
+    then
+        SPACK_INSTALLED_COMPILERS="$(spack compilers | egrep -v "^(-|=)")"
+    fi
+    SPACK_COMPREPLY="$SPACK_INSTALLED_COMPILERS"
+}
+
+_providers() {
+    if [[ -z "${SPACK_PROVIDERS:-}" ]]
+    then
+        SPACK_PROVIDERS="$(spack providers)"
+    fi
+    SPACK_COMPREPLY="$SPACK_PROVIDERS"
+}
+
+_mirrors() {
+    if [[ -z "${SPACK_MIRRORS:-}" ]]
+    then
+        SPACK_MIRRORS="$(spack mirror list | awk '{print $1}')"
+    fi
+    SPACK_COMPREPLY="$SPACK_MIRRORS"
+}
+
+_repos() {
+    if [[ -z "${SPACK_REPOS:-}" ]]
+    then
+        SPACK_REPOS="$(spack repo list | awk '{print $1}')"
+    fi
+    SPACK_COMPREPLY="$SPACK_REPOS"
+}
+
+_tests() {
+    if [[ -z "${SPACK_TESTS:-}" ]]
+    then
+        SPACK_TESTS="$(spack test -l)"
+    fi
+    SPACK_COMPREPLY="$SPACK_TESTS"
+}
+
+_environments() {
+    if [[ -z "${SPACK_ENVIRONMENTS:-}" ]]
+    then
+        SPACK_ENVIRONMENTS="$(spack env list)"
+    fi
+    SPACK_COMPREPLY="$SPACK_ENVIRONMENTS"
+}
+
+_keys() {
+    if [[ -z "${SPACK_KEYS:-}" ]]
+    then
+        SPACK_KEYS="$(spack gpg list)"
+    fi
+    SPACK_COMPREPLY="$SPACK_KEYS"
+}
+
+_config_sections() {
+    if [[ -z "${SPACK_CONFIG_SECTIONS:-}" ]]
+    then
+        SPACK_CONFIG_SECTIONS="compilers mirrors repos packages modules config upstreams"
+    fi
+    SPACK_COMPREPLY="$SPACK_CONFIG_SECTIONS"
+}
+
+_extensions() {
+    if [[ -z "${SPACK_EXTENSIONS:-}" ]]
+    then
+        SPACK_EXTENSIONS="aspell go-bootstrap go icedtea jdk kim-api lua matlab mofem-cephas octave openjdk perl python r ruby rust tcl yorick"
+    fi
+    SPACK_COMPREPLY="$SPACK_EXTENSIONS"
+}
+
+# Testing functions
+
+# Function for unit testing tab completion
+# Syntax: _spack_completions spack install py-
+_spack_completions() {
+    local COMP_CWORD COMP_KEY COMP_LINE COMP_POINT COMP_TYPE COMP_WORDS COMPREPLY
+
+    # Set each variable the way bash would
+    COMP_LINE="$*"
+    COMP_POINT=${#COMP_LINE}
+    COMP_WORDS=("$@")
+    if [[ ${COMP_LINE: -1} == ' ' ]]
+    then
+        COMP_WORDS+=('')
+    fi
+    COMP_CWORD=$((${#COMP_WORDS[@]} - 1))
+    COMP_KEY=9    # ASCII 09: Horizontal Tab
+    COMP_TYPE=64  # ASCII 64: '@', to list completions if the word is not unmodified
+
+    # Run Spack's tab completion function
+    _bash_completion_spack
+
+    # Return the result
+    echo "${COMPREPLY[@]:-}"
+}
+
+# Log the environment variables used
+# Syntax: _test_vars >> temp
+_test_vars() {
+    echo "-----------------------------------------------------"
+    echo "Variables set by bash:"
+    echo
+    echo "COMP_LINE:                '$COMP_LINE'"
+    echo "# COMP_LINE:              '${#COMP_LINE}'"
+    echo "COMP_WORDS:               $(_pretty_print COMP_WORDS[@])"
+    echo "# COMP_WORDS:             '${#COMP_WORDS[@]}'"
+    echo "COMP_CWORD:               '$COMP_CWORD'"
+    echo "COMP_KEY:                 '$COMP_KEY'"
+    echo "COMP_POINT:               '$COMP_POINT'"
+    echo "COMP_TYPE:                '$COMP_TYPE'"
+    echo "COMP_WORDBREAKS:          '$COMP_WORDBREAKS'"
+    echo
+    echo "Intermediate variables:"
+    echo
+    echo "COMP_WORDS_NO_FLAGS:      $(_pretty_print COMP_WORDS_NO_FLAGS[@])"
+    echo "# COMP_WORDS_NO_FLAGS:    '${#COMP_WORDS_NO_FLAGS[@]}'"
+    echo "COMP_CWORD_NO_FLAGS:      '$COMP_CWORD_NO_FLAGS'"
+    echo
+    echo "Subfunction:              '$subfunction'"
+    if $list_options
+    then
+        echo "List options:             'True'"
+    else
+        echo "List options:             'False'"
     fi
+    echo "Current word:             '$cur'"
 }
 
+# Pretty-prints one or more arrays
+# Syntax: _pretty_print array1[@] ...
+_pretty_print() {
+    for arg in $@
+    do
+        local array=("${!arg}")
+        printf "$arg: ["
+        printf   "'%s'" "${array[0]}"
+        printf ", '%s'" "${array[@]:1}"
+        echo "]"
+    done
+}
+
+complete -o bashdefault -o default -F _bash_completion_spack spack
+
 # Spack commands
+#
+# Everything below here is auto-generated.
 
-_spack () {
+_spack() {
     if $list_options
     then
-        compgen -W "-h --help -H --all-help --color -C --config-scope -d --debug --timestamp --pdb -e --env -D --env-dir -E --no-env --use-env-repo -k --insecure -l --enable-locks -L --disable-locks -m --mock -p --profile --sorted-profile --lines -v --verbose --stacktrace -V --version --print-shell-vars" -- "$cur"
+        SPACK_COMPREPLY="-h --help -H --all-help --color -C --config-scope -d --debug --timestamp --pdb -e --env -D --env-dir -E --no-env --use-env-repo -k --insecure -l --enable-locks -L --disable-locks -m --mock -p --profile --sorted-profile --lines -v --verbose --stacktrace -V --version --print-shell-vars"
     else
-        compgen -W "$(_subcommands)" -- "$cur"
+        SPACK_COMPREPLY="activate add arch blame bootstrap build build-env buildcache cd checksum ci clean clone commands compiler compilers concretize config configure create deactivate debug dependencies dependents deprecate dev-build diy docs edit env extensions fetch find flake8 gc gpg graph help info install license list load location log-parse maintainers mirror module patch pkg providers pydoc python reindex remove rm repo resource restage setup spec stage test uninstall unload upload-s3 url verify versions view"
     fi
 }
 
-_spack_activate () {
+_spack_activate() {
     if $list_options
     then
-        compgen -W "-h --help -f --force -v --view" -- "$cur"
+        SPACK_COMPREPLY="-h --help -f --force -v --view"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_add () {
+_spack_add() {
     if $list_options
     then
-        compgen -W "-h --help -l --list-name" -- "$cur"
+        SPACK_COMPREPLY="-h --help -l --list-name"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_arch () {
-    compgen -W "-h --help --known-targets -p --platform -o --operating-system -t --target -f --frontend -b --backend" -- "$cur"
+_spack_arch() {
+    SPACK_COMPREPLY="-h --help --known-targets -p --platform -o --operating-system -t --target -f --frontend -b --backend"
 }
 
-_spack_blame () {
+_spack_blame() {
     if $list_options
     then
-        compgen -W "-h --help -t --time -p --percent -g --git" -- "$cur"
+        SPACK_COMPREPLY="-h --help -t --time -p --percent -g --git"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_bootstrap () {
-    compgen -W "-h --help -j --jobs --keep-prefix --keep-stage -n --no-checksum -v --verbose --use-cache --no-cache --cache-only --clean --dirty" -- "$cur"
+_spack_bootstrap() {
+    SPACK_COMPREPLY="-h --help -j --jobs --keep-prefix --keep-stage -n --no-checksum -v --verbose --use-cache --no-cache --cache-only --clean --dirty"
 }
 
-_spack_build () {
+_spack_build() {
     if $list_options
     then
-        compgen -W "-h --help -v --verbose" -- "$cur"
+        SPACK_COMPREPLY="-h --help -v --verbose"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_build_env () {
+_spack_build_env() {
     if $list_options
     then
-        compgen -W "-h --help --clean --dirty --dump --pickle" -- "$cur"
+        SPACK_COMPREPLY="-h --help --clean --dirty --dump --pickle"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_buildcache () {
+_spack_buildcache() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "create install list keys preview check download get-buildcache-name save-yaml copy update-index" -- "$cur"
+        SPACK_COMPREPLY="create install list keys preview check download get-buildcache-name save-yaml copy update-index"
     fi
 }
 
-_spack_buildcache_create () {
+_spack_buildcache_create() {
     if $list_options
     then
-        compgen -W "-h --help -r --rel -f --force -u --unsigned -a --allow-root -k --key -d --directory --no-rebuild-index -y --spec-yaml --no-deps" -- "$cur"
+        SPACK_COMPREPLY="-h --help -r --rel -f --force -u --unsigned -a --allow-root -k --key -d --directory --no-rebuild-index -y --spec-yaml --no-deps"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_buildcache_install () {
+_spack_buildcache_install() {
     if $list_options
     then
-        compgen -W "-h --help -f --force -m --multiple -a --allow-root -u --unsigned" -- "$cur"
+        SPACK_COMPREPLY="-h --help -f --force -m --multiple -a --allow-root -u --unsigned"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_buildcache_list () {
+_spack_buildcache_list() {
     if $list_options
     then
-        compgen -W "-h --help -l --long -L --very-long -v --variants -f --force" -- "$cur"
+        SPACK_COMPREPLY="-h --help -l --long -L --very-long -v --variants -f --force"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_buildcache_keys () {
-    compgen -W "-h --help -i --install -t --trust -f --force" -- "$cur"
+_spack_buildcache_keys() {
+    SPACK_COMPREPLY="-h --help -i --install -t --trust -f --force"
 }
 
-_spack_buildcache_preview () {
+_spack_buildcache_preview() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_buildcache_check () {
-    compgen -W "-h --help -m --mirror-url -o --output-file --scope -s --spec -y --spec-yaml --rebuild-on-error" -- "$cur"
+_spack_buildcache_check() {
+    SPACK_COMPREPLY="-h --help -m --mirror-url -o --output-file --scope -s --spec -y --spec-yaml --rebuild-on-error"
 }
 
-_spack_buildcache_download () {
-    compgen -W "-h --help -s --spec -y --spec-yaml -p --path -c --require-cdashid" -- "$cur"
+_spack_buildcache_download() {
+    SPACK_COMPREPLY="-h --help -s --spec -y --spec-yaml -p --path -c --require-cdashid"
 }
 
-_spack_buildcache_get_buildcache_name () {
-    compgen -W "-h --help -s --spec -y --spec-yaml" -- "$cur"
+_spack_buildcache_get_buildcache_name() {
+    SPACK_COMPREPLY="-h --help -s --spec -y --spec-yaml"
 }
 
-_spack_buildcache_save_yaml () {
-    compgen -W "-h --help --root-spec --root-spec-yaml -s --specs -y --yaml-dir" -- "$cur"
+_spack_buildcache_save_yaml() {
+    SPACK_COMPREPLY="-h --help --root-spec --root-spec-yaml -s --specs -y --yaml-dir"
 }
 
-_spack_buildcache_copy () {
-    compgen -W "-h --help --base-dir --spec-yaml --destination-url" -- "$cur"
+_spack_buildcache_copy() {
+    SPACK_COMPREPLY="-h --help --base-dir --spec-yaml --destination-url"
 }
 
-_spack_buildcache_update_index () {
-    compgen -W "-h --help -d --mirror-url" -- "$cur"
+_spack_buildcache_update_index() {
+    SPACK_COMPREPLY="-h --help -d --mirror-url"
 }
 
-_spack_cd () {
+_spack_cd() {
     if $list_options
     then
-        compgen -W "-h --help -m --module-dir -r --spack-root -i --install-dir -p --package-dir -P --packages -s --stage-dir -S --stages -b --build-dir -e --env" -- "$cur"
+        SPACK_COMPREPLY="-h --help -m --module-dir -r --spack-root -i --install-dir -p --package-dir -P --packages -s --stage-dir -S --stages -b --build-dir -e --env"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_checksum () {
+_spack_checksum() {
     if $list_options
     then
-        compgen -W "-h --help --keep-stage" -- "$cur"
+        SPACK_COMPREPLY="-h --help --keep-stage"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_clean () {
+_spack_ci() {
     if $list_options
     then
-        compgen -W "-h --help -s --stage -d --downloads -m --misc-cache -p --python-cache -a --all" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        SPACK_COMPREPLY="start generate pushyaml rebuild"
     fi
 }
 
-_spack_clone () {
+_spack_ci_start() {
+    SPACK_COMPREPLY="-h --help --output-file --copy-to --spack-repo --spack-ref --downstream-repo --branch-name --commit-sha"
+}
+
+_spack_ci_generate() {
+    SPACK_COMPREPLY="-h --help --output-file --copy-to --spack-repo --spack-ref"
+}
+
+_spack_ci_pushyaml() {
+    SPACK_COMPREPLY="-h --help --downstream-repo --branch-name --commit-sha"
+}
+
+_spack_ci_rebuild() {
+    SPACK_COMPREPLY="-h --help"
+}
+
+_spack_clean() {
     if $list_options
     then
-        compgen -W "-h --help -r --remote" -- "$cur"
+        SPACK_COMPREPLY="-h --help -s --stage -d --downloads -m --misc-cache -p --python-cache -a --all"
+    else
+        _all_packages
     fi
 }
 
-_spack_commands () {
+_spack_clone() {
     if $list_options
     then
-        compgen -W "-h --help --format --header --update" -- "$cur"
+        SPACK_COMPREPLY="-h --help -r --remote"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_compiler () {
+_spack_commands() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help -a --aliases --format --header --update"
     else
-        compgen -W "find add remove rm list info" -- "$cur"
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_compiler_find () {
+_spack_compiler() {
     if $list_options
     then
-        compgen -W "-h --help --scope" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
+    else
+        SPACK_COMPREPLY="find add remove rm list info"
     fi
 }
 
-_spack_compiler_add () {
-    # Alias to `spack compiler find`
-    _spack_compiler_find
+_spack_compiler_find() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help --scope"
+    else
+        SPACK_COMPREPLY=""
+    fi
 }
 
-_spack_compiler_remove () {
+_spack_compiler_add() {
     if $list_options
     then
-        compgen -W "-h --help -a --all --scope" -- "$cur"
+        SPACK_COMPREPLY="-h --help --scope"
     else
-        compgen -W "$(_installed_compilers)" -- "$cur"
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_compiler_rm () {
-    # Alias to `spack compiler remove`
-    _spack_compiler_remove
+_spack_compiler_remove() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help -a --all --scope"
+    else
+        _installed_compilers
+    fi
+}
+
+_spack_compiler_rm() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help -a --all --scope"
+    else
+        _installed_compilers
+    fi
 }
 
-_spack_compiler_list () {
-    compgen -W "-h --help --scope" -- "$cur"
+_spack_compiler_list() {
+    SPACK_COMPREPLY="-h --help --scope"
 }
 
-_spack_compiler_info () {
+_spack_compiler_info() {
     if $list_options
     then
-        compgen -W "-h --help --scope" -- "$cur"
+        SPACK_COMPREPLY="-h --help --scope"
     else
-        compgen -W "$(_installed_compilers)" -- "$cur"
+        _installed_compilers
     fi
 }
 
-_spack_compilers () {
-    # Alias to `spack compiler list`
-    _spack_compiler_list
+_spack_compilers() {
+    SPACK_COMPREPLY="-h --help --scope"
 }
 
-_spack_concretize () {
-    compgen -W "-h --help -f --force" -- "$cur"
+_spack_concretize() {
+    SPACK_COMPREPLY="-h --help -f --force"
 }
 
-_spack_config () {
+_spack_config() {
     if $list_options
     then
-        compgen -W "-h --help --scope" -- "$cur"
+        SPACK_COMPREPLY="-h --help --scope"
     else
-        compgen -W "get blame edit" -- "$cur"
+        SPACK_COMPREPLY="get blame edit"
     fi
 }
 
-_spack_config_get () {
+_spack_config_get() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "compilers mirrors repos packages modules config upstreams" -- "$cur"
+        _config_sections
     fi
 }
 
-_spack_config_blame () {
+_spack_config_blame() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "compilers mirrors repos packages modules config upstreams" -- "$cur"
+        _config_sections
     fi
 }
 
-_spack_config_edit () {
+_spack_config_edit() {
     if $list_options
     then
-        compgen -W "-h --help --print-file" -- "$cur"
+        SPACK_COMPREPLY="-h --help --print-file"
     else
-        compgen -W "compilers mirrors repos packages modules config upstreams" -- "$cur"
+        _config_sections
     fi
 }
 
-_spack_configure () {
+_spack_configure() {
     if $list_options
     then
-        compgen -W "-h --help -v --verbose" -- "$cur"
+        SPACK_COMPREPLY="-h --help -v --verbose"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_create () {
+_spack_create() {
     if $list_options
     then
-        compgen -W "-h --help --keep-stage -n --name -t --template -r --repo -N --namespace -f --force --skip-editor" -- "$cur"
+        SPACK_COMPREPLY="-h --help --keep-stage -n --name -t --template -r --repo -N --namespace -f --force --skip-editor"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_deactivate () {
+_spack_deactivate() {
     if $list_options
     then
-        compgen -W "-h --help -f --force -v --view -a --all" -- "$cur"
+        SPACK_COMPREPLY="-h --help -f --force -v --view -a --all"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_debug () {
+_spack_debug() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "create-db-tarball" -- "$cur"
+        SPACK_COMPREPLY="create-db-tarball"
     fi
 }
 
-_spack_debug_create_db_tarball () {
-    compgen -W "-h --help" -- "$cur"
+_spack_debug_create_db_tarball() {
+    SPACK_COMPREPLY="-h --help"
 }
 
-_spack_dependencies () {
+_spack_dependencies() {
     if $list_options
     then
-        compgen -W "-h --help -i --installed -t --transitive --deptype -V --no-expand-virtuals" -- "$cur"
+        SPACK_COMPREPLY="-h --help -i --installed -t --transitive --deptype -V --no-expand-virtuals"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_dependents () {
+_spack_dependents() {
     if $list_options
     then
-        compgen -W "-h --help -i --installed -t --transitive" -- "$cur"
+        SPACK_COMPREPLY="-h --help -i --installed -t --transitive"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_deprecate () {
+_spack_deprecate() {
     if $list_options
     then
-        compgen -W "-h --help -y --yes-to-all -d --dependencies -D --no-dependencies -i --install-deprecator -I --no-install-deprecator -l --link-type" -- "$cur"
+        SPACK_COMPREPLY="-h --help -y --yes-to-all -d --dependencies -D --no-dependencies -i --install-deprecator -I --no-install-deprecator -l --link-type"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_dev_build () {
+_spack_dev_build() {
     if $list_options
     then
-        compgen -W "-h --help -j --jobs -d --source-path -i --ignore-dependencies -n --no-checksum --keep-prefix --skip-patch -q --quiet -u --until --clean --dirty" -- "$cur"
+        SPACK_COMPREPLY="-h --help -j --jobs -d --source-path -i --ignore-dependencies -n --no-checksum --keep-prefix --skip-patch -q --quiet -u --until --clean --dirty"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_diy () {
+_spack_diy() {
     if $list_options
     then
-        compgen -W "-h --help -j --jobs -d --source-path -i --ignore-dependencies -n --no-checksum --keep-prefix --skip-patch -q --quiet -u --until --clean --dirty" -- "$cur"
+        SPACK_COMPREPLY="-h --help -j --jobs -d --source-path -i --ignore-dependencies -n --no-checksum --keep-prefix --skip-patch -q --quiet -u --until --clean --dirty"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_docs () {
-    compgen -W "-h --help" -- "$cur"
+_spack_docs() {
+    SPACK_COMPREPLY="-h --help"
 }
 
-_spack_edit () {
+_spack_edit() {
     if $list_options
     then
-        compgen -W "-h --help -b --build-system -c --command -d --docs -t --test -m --module -r --repo -N --namespace" -- "$cur"
+        SPACK_COMPREPLY="-h --help -b --build-system -c --command -d --docs -t --test -m --module -r --repo -N --namespace"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_env () {
+_spack_env() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "activate deactivate create remove rm list ls status st loads view" -- "$cur"
+        SPACK_COMPREPLY="activate deactivate create remove rm list ls status st loads view"
     fi
 }
 
-_spack_env_activate () {
+_spack_env_activate() {
     if $list_options
     then
-        compgen -W "-h --help --sh --csh -v --with-view -V --without-view -d --dir -p --prompt" -- "$cur"
+        SPACK_COMPREPLY="-h --help --sh --csh -v --with-view -V --without-view -d --dir -p --prompt"
     else
-        compgen -W "$(_environments)" -- "$cur"
+        _environments
     fi
 }
 
-_spack_env_deactivate () {
-    compgen -W "-h --help --sh --csh" -- "$cur"
+_spack_env_deactivate() {
+    SPACK_COMPREPLY="-h --help --sh --csh"
 }
 
-_spack_env_create () {
+_spack_env_create() {
     if $list_options
     then
-        compgen -W "-h --help -d --dir --without-view --with-view" -- "$cur"
+        SPACK_COMPREPLY="-h --help -d --dir --without-view --with-view"
+    else
+        _environments
     fi
 }
 
-_spack_env_remove () {
+_spack_env_remove() {
     if $list_options
     then
-        compgen -W "-h --help -y --yes-to-all" -- "$cur"
+        SPACK_COMPREPLY="-h --help -y --yes-to-all"
     else
-        compgen -W "$(_environments)" -- "$cur"
+        _environments
     fi
 }
 
-_spack_env_rm () {
-    # Alias to `spack env remove`
-    _spack_env_remove
+_spack_env_rm() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help -y --yes-to-all"
+    else
+        _environments
+    fi
 }
 
-_spack_env_list () {
-    compgen -W "-h --help" -- "$cur"
+_spack_env_list() {
+    SPACK_COMPREPLY="-h --help"
 }
 
-_spack_env_ls () {
-    # Alias to `spack env list`
-    _spack_env_list
+_spack_env_ls() {
+    SPACK_COMPREPLY="-h --help"
 }
 
-_spack_env_status () {
-    compgen -W "-h --help" -- "$cur"
+_spack_env_status() {
+    SPACK_COMPREPLY="-h --help"
 }
 
-_spack_env_st () {
-    # Alias to `spack env status`
-    _spack_env_status
+_spack_env_st() {
+    SPACK_COMPREPLY="-h --help"
 }
 
-_spack_env_loads () {
+_spack_env_loads() {
     if $list_options
     then
-        compgen -W "-h --help -m --module-type --input-only -p --prefix -x --exclude -r --dependencies" -- "$cur"
+        SPACK_COMPREPLY="-h --help -m --module-type --input-only -p --prefix -x --exclude -r --dependencies"
     else
-        compgen -W "$(_environments)" -- "$cur"
+        _environments
     fi
 }
 
-_spack_env_view () {
+_spack_env_view() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "regenerate enable disable" -- "$cur"
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_extensions () {
+_spack_extensions() {
     if $list_options
     then
-        compgen -W "-h --help -l --long -L --very-long -d --deps -p --paths -s --show -v --view" -- "$cur"
+        SPACK_COMPREPLY="-h --help -l --long -L --very-long -d --deps -p --paths -s --show -v --view"
     else
-        compgen -W "aspell go-bootstrap go icedtea jdk kim-api lua matlab mofem-cephas octave openjdk perl python r ruby rust tcl yorick" -- "$cur"
+        _extensions
     fi
 }
 
-_spack_fetch () {
+_spack_fetch() {
     if $list_options
     then
-        compgen -W "-h --help -n --no-checksum -m --missing -D --dependencies" -- "$cur"
+        SPACK_COMPREPLY="-h --help -n --no-checksum -m --missing -D --dependencies"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_find () {
+_spack_find() {
     if $list_options
     then
-        compgen -W "-h --help --format --json -d --deps -p --paths --groups --no-groups -l --long -L --very-long -t --tags -c --show-concretized -f --show-flags --show-full-compiler -x --explicit -X --implicit -u --unknown -m --missing -v --variants -M --only-missing --deprecated --only-deprecated -N --namespace --start-date --end-date" -- "$cur"
+        SPACK_COMPREPLY="-h --help --format --json -d --deps -p --paths --groups --no-groups -l --long -L --very-long -t --tags -c --show-concretized -f --show-flags --show-full-compiler -x --explicit -X --implicit -u --unknown -m --missing -v --variants -M --only-missing --deprecated --only-deprecated -N --namespace --start-date --end-date"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_flake8 () {
+_spack_flake8() {
     if $list_options
     then
-        compgen -W "-h --help -b --base -k --keep-temp -a --all -o --output -r --root-relative -U --no-untracked" -- "$cur"
+        SPACK_COMPREPLY="-h --help -b --base -k --keep-temp -a --all -o --output -r --root-relative -U --no-untracked"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_gpg () {
+_spack_gc() {
+    SPACK_COMPREPLY="-h --help -y --yes-to-all"
+}
+
+_spack_gpg() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "verify trust untrust sign create list init export" -- "$cur"
+        SPACK_COMPREPLY="verify trust untrust sign create list init export"
     fi
 }
 
-_spack_gpg_verify () {
+_spack_gpg_verify() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "$(installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_gpg_trust () {
+_spack_gpg_trust() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_gpg_untrust () {
+_spack_gpg_untrust() {
     if $list_options
     then
-        compgen -W "-h --help --signing" -- "$cur"
+        SPACK_COMPREPLY="-h --help --signing"
     else
-        compgen -W "$(_keys)" -- "$cur"
+        _keys
     fi
 }
 
-_spack_gpg_sign () {
+_spack_gpg_sign() {
     if $list_options
     then
-        compgen -W "-h --help --output --key --clearsign" -- "$cur"
+        SPACK_COMPREPLY="-h --help --output --key --clearsign"
     else
-        compgen -W "$(installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_gpg_create () {
+_spack_gpg_create() {
     if $list_options
     then
-        compgen -W "-h --help --comment --expires --export" -- "$cur"
+        SPACK_COMPREPLY="-h --help --comment --expires --export"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_gpg_list () {
-    compgen -W "-h --help --trusted --signing" -- "$cur"
+_spack_gpg_list() {
+    SPACK_COMPREPLY="-h --help --trusted --signing"
 }
 
-_spack_gpg_init () {
-    compgen -W "-h --help" -- "$cur"
+_spack_gpg_init() {
+    SPACK_COMPREPLY="-h --help --from"
 }
 
-_spack_gpg_export () {
+_spack_gpg_export() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "$(_keys)" -- "$cur"
+        _keys
     fi
 }
 
-_spack_graph () {
+_spack_graph() {
     if $list_options
     then
-        compgen -W "-h --help -a --ascii -d --dot -s --static -i --installed --deptype" -- "$cur"
+        SPACK_COMPREPLY="-h --help -a --ascii -d --dot -s --static -i --installed --deptype"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_help () {
+_spack_help() {
     if $list_options
     then
-        compgen -W "-h --help -a --all --spec" -- "$cur"
+        SPACK_COMPREPLY="-h --help -a --all --spec"
     else
-        compgen -W "$(_subcommands)" -- "$cur"
+        _subcommands
     fi
 }
 
-_spack_info () {
+_spack_info() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_install () {
+_spack_install() {
     if $list_options
     then
-        compgen -W "-h --help --only -u --until -j --jobs --overwrite --keep-prefix --keep-stage --dont-restage --use-cache --no-cache --cache-only --show-log-on-error --source -n --no-checksum -v --verbose --fake --only-concrete -f --file --clean --dirty --test --run-tests --log-format --log-file --help-cdash -y --yes-to-all --cdash-upload-url --cdash-build --cdash-site --cdash-track --cdash-buildstamp" -- "$cur"
+        SPACK_COMPREPLY="-h --help --only -u --until -j --jobs --overwrite --keep-prefix --keep-stage --dont-restage --use-cache --no-cache --cache-only --show-log-on-error --source -n --no-checksum -v --verbose --fake --only-concrete -f --file --clean --dirty --test --run-tests --log-format --log-file --help-cdash --cdash-upload-url --cdash-build --cdash-site --cdash-track --cdash-buildstamp -y --yes-to-all"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_license () {
+_spack_license() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "list-files verify" -- "$cur"
+        SPACK_COMPREPLY="list-files verify"
     fi
 }
 
-_spack_license_list_files () {
-    compgen -W "-h --help" -- "$cur"
+_spack_license_list_files() {
+    SPACK_COMPREPLY="-h --help"
 }
 
-_spack_license_verify () {
-    compgen -W "-h --help --root" -- "$cur"
+_spack_license_verify() {
+    SPACK_COMPREPLY="-h --help --root"
 }
 
-_spack_list () {
+_spack_list() {
     if $list_options
     then
-        compgen -W "-h --help -d --search-description --format --update -t --tags" -- "$cur"
+        SPACK_COMPREPLY="-h --help -d --search-description --format --update -t --tags"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_load () {
+_spack_load() {
     if $list_options
     then
-        compgen -W "-h --help -r --dependencies" -- "$cur"
+        SPACK_COMPREPLY="-h --help -r --dependencies"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_location () {
+_spack_location() {
     if $list_options
     then
-        compgen -W "-h --help -m --module-dir -r --spack-root -i --install-dir -p --package-dir -P --packages -s --stage-dir -S --stages -b --build-dir -e --env" -- "$cur"
+        SPACK_COMPREPLY="-h --help -m --module-dir -r --spack-root -i --install-dir -p --package-dir -P --packages -s --stage-dir -S --stages -b --build-dir -e --env"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_log_parse () {
+_spack_log_parse() {
     if $list_options
     then
-        compgen -W "-h --help --show -c --context -p --profile -w --width -j --jobs" -- "$cur"
+        SPACK_COMPREPLY="-h --help --show -c --context -p --profile -w --width -j --jobs"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_maintainers () {
+_spack_maintainers() {
     if $list_options
     then
-        compgen -W "-h --help --maintained --unmaintained -a --all --by-user" -- "$cur"
+        SPACK_COMPREPLY="-h --help --maintained --unmaintained -a --all --by-user"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_mirror () {
+_spack_mirror() {
     if $list_options
     then
-        compgen -W "-h --help -n --no-checksum" -- "$cur"
+        SPACK_COMPREPLY="-h --help -n --no-checksum"
     else
-        compgen -W "create add remove rm set-url list" -- "$cur"
+        SPACK_COMPREPLY="create add remove rm set-url list"
     fi
 }
 
-_spack_mirror_create () {
+_spack_mirror_create() {
     if $list_options
     then
-        compgen -W "-h --help -d --directory -a --all -f --file -D --dependencies -n --versions-per-spec" -- "$cur"
+        SPACK_COMPREPLY="-h --help -d --directory -a --all -f --file -D --dependencies -n --versions-per-spec"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_mirror_add () {
+_spack_mirror_add() {
     if $list_options
     then
-        compgen -W "-h --help --scope" -- "$cur"
+        SPACK_COMPREPLY="-h --help --scope"
+    else
+        _mirrors
     fi
 }
 
-_spack_mirror_remove () {
+_spack_mirror_remove() {
     if $list_options
     then
-        compgen -W "-h --help --scope" -- "$cur"
+        SPACK_COMPREPLY="-h --help --scope"
     else
-        compgen -W "$(_mirrors)" -- "$cur"
+        _mirrors
     fi
 }
 
-_spack_mirror_rm () {
-    # Alias to `spack mirror remove`
-    _spack_mirror_remove
+_spack_mirror_rm() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help --scope"
+    else
+        _mirrors
+    fi
 }
 
-_spack_mirror_set_url () {
+_spack_mirror_set_url() {
     if $list_options
     then
-        compgen -W "-h --help --push --scope" -- "$cur"
+        SPACK_COMPREPLY="-h --help --push --scope"
     else
-        compgen -W "$(_mirrors)" -- "$cur"
+        _mirrors
     fi
 }
 
-_spack_mirror_list () {
-    compgen -W "-h --help --scope" -- "$cur"
+_spack_mirror_list() {
+    SPACK_COMPREPLY="-h --help --scope"
 }
 
-_spack_module () {
+_spack_module() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "lmod tcl" -- "$cur"
+        SPACK_COMPREPLY="lmod tcl"
     fi
 }
 
-_spack_module_lmod () {
+_spack_module_lmod() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "refresh find rm loads setdefault" -- "$cur"
+        SPACK_COMPREPLY="refresh find rm loads setdefault"
     fi
 }
 
-_spack_module_lmod_refresh () {
+_spack_module_lmod_refresh() {
     if $list_options
     then
-        compgen -W "-h --help --delete-tree --upstream-modules -y --yes-to-all" -- "$cur"
+        SPACK_COMPREPLY="-h --help --delete-tree --upstream-modules -y --yes-to-all"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_module_lmod_find () {
+_spack_module_lmod_find() {
     if $list_options
     then
-        compgen -W "-h --help --full-path -r --dependencies" -- "$cur"
+        SPACK_COMPREPLY="-h --help --full-path -r --dependencies"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_module_lmod_rm () {
+_spack_module_lmod_rm() {
     if $list_options
     then
-        compgen -W "-h --help -y --yes-to-all" -- "$cur"
+        SPACK_COMPREPLY="-h --help -y --yes-to-all"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_module_lmod_loads () {
+_spack_module_lmod_loads() {
     if $list_options
     then
-        compgen -W "-h --help --input-only -p --prefix -x --exclude -r --dependencies" -- "$cur"
+        SPACK_COMPREPLY="-h --help --input-only -p --prefix -x --exclude -r --dependencies"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
-
 }
 
-_spack_module_lmod_setdefault () {
+_spack_module_lmod_setdefault() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_module_tcl () {
+_spack_module_tcl() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "refresh find rm loads" -- "$cur"
+        SPACK_COMPREPLY="refresh find rm loads"
     fi
 }
 
-_spack_module_tcl_refresh () {
+_spack_module_tcl_refresh() {
     if $list_options
     then
-        compgen -W "-h --help --delete-tree --upstream-modules -y --yes-to-all" -- "$cur"
+        SPACK_COMPREPLY="-h --help --delete-tree --upstream-modules -y --yes-to-all"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_module_tcl_find () {
+_spack_module_tcl_find() {
     if $list_options
     then
-        compgen -W "-h --help --full-path -r --dependencies" -- "$cur"
+        SPACK_COMPREPLY="-h --help --full-path -r --dependencies"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_module_tcl_rm () {
+_spack_module_tcl_rm() {
     if $list_options
     then
-        compgen -W "-h --help -y --yes-to-all" -- "$cur"
+        SPACK_COMPREPLY="-h --help -y --yes-to-all"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_module_tcl_loads () {
+_spack_module_tcl_loads() {
     if $list_options
     then
-        compgen -W "-h --help --input-only -p --prefix -x --exclude -r --dependencies" -- "$cur"
+        SPACK_COMPREPLY="-h --help --input-only -p --prefix -x --exclude -r --dependencies"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_patch () {
+_spack_patch() {
     if $list_options
     then
-        compgen -W "-h --help -n --no-checksum" -- "$cur"
+        SPACK_COMPREPLY="-h --help -n --no-checksum"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_pkg () {
+_spack_pkg() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "add list diff added changed removed" -- "$cur"
+        SPACK_COMPREPLY="add list diff added changed removed"
     fi
 }
 
-_spack_pkg_add () {
+_spack_pkg_add() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_pkg_list () {
-    # FIXME: How to list git revisions?
+_spack_pkg_list() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_pkg_diff () {
-    # FIXME: How to list git revisions?
+_spack_pkg_diff() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_pkg_added () {
-    # FIXME: How to list git revisions?
+_spack_pkg_added() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_pkg_changed () {
-    # FIXME: How to list git revisions?
+_spack_pkg_changed() {
     if $list_options
     then
-        compgen -W "-h --help -t --type" -- "$cur"
+        SPACK_COMPREPLY="-h --help -t --type"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_pkg_removed () {
-    # FIXME: How to list git revisions?
+_spack_pkg_removed() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_providers () {
+_spack_providers() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "$(_providers)" -- "$cur"
+        _providers
     fi
 }
 
-_spack_pydoc () {
+_spack_pydoc() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_python () {
+_spack_python() {
     if $list_options
     then
-        compgen -W "-h --help -c" -- "$cur"
+        SPACK_COMPREPLY="-h --help -c"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_reindex () {
-    compgen -W "-h --help" -- "$cur"
+_spack_reindex() {
+    SPACK_COMPREPLY="-h --help"
 }
 
-_spack_release_jobs () {
-    compgen -W "-h --help -o --output-file -p --print-summary --cdash-credentials" -- "$cur"
+_spack_remove() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help -a --all -l --list-name -f --force"
+    else
+        _all_packages
+    fi
 }
 
-_spack_remove () {
+_spack_rm() {
     if $list_options
     then
-        compgen -W "-h --help -a --all -l --list-name -f --force" -- "$cur"
+        SPACK_COMPREPLY="-h --help -a --all -l --list-name -f --force"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_repo () {
+_spack_repo() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "create list add remove rm" -- "$cur"
+        SPACK_COMPREPLY="create list add remove rm"
     fi
 }
 
-_spack_repo_create () {
+_spack_repo_create() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
+    else
+        _repos
     fi
 }
 
-_spack_repo_list () {
-    compgen -W "-h --help --scope" -- "$cur"
+_spack_repo_list() {
+    SPACK_COMPREPLY="-h --help --scope"
 }
 
-_spack_repo_add () {
+_spack_repo_add() {
     if $list_options
     then
-        compgen -W "-h --help --scope" -- "$cur"
+        SPACK_COMPREPLY="-h --help --scope"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_repo_remove () {
+_spack_repo_remove() {
     if $list_options
     then
-        compgen -W "-h --help --scope" -- "$cur"
+        SPACK_COMPREPLY="-h --help --scope"
     else
-        compgen -W "$(_repos)" -- "$cur"
+        _repos
     fi
 }
 
-_spack_repo_rm () {
-    # Alias to `spack repo remove`
-    _spack_repo_remove
+_spack_repo_rm() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help --scope"
+    else
+        _repos
+    fi
 }
 
-_spack_resource () {
+_spack_resource() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "list show" -- "$cur"
+        SPACK_COMPREPLY="list show"
     fi
 }
 
-_spack_resource_list () {
-    compgen -W "-h --help --only-hashes" -- "$cur"
+_spack_resource_list() {
+    SPACK_COMPREPLY="-h --help --only-hashes"
 }
 
-_spack_resource_show () {
+_spack_resource_show() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "$(_all_resource_hashes)" -- "$cur"
+        _all_resource_hashes
     fi
 }
 
-_spack_restage () {
+_spack_restage() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_setup () {
+_spack_setup() {
     if $list_options
     then
-        compgen -W "-h --help -i --ignore-dependencies -n --no-checksum -v --verbose --clean --dirty" -- "$cur"
+        SPACK_COMPREPLY="-h --help -i --ignore-dependencies -n --no-checksum -v --verbose --clean --dirty"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_spec () {
+_spack_spec() {
     if $list_options
     then
-        compgen -W "-h --help -l --long -L --very-long -I --install-status -y --yaml -j --json -c --cover -N --namespaces -t --types" -- "$cur"
+        SPACK_COMPREPLY="-h --help -l --long -L --very-long -I --install-status -y --yaml -j --json -c --cover -N --namespaces -t --types"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_stage () {
+_spack_stage() {
     if $list_options
     then
-        compgen -W "-h --help -n --no-checksum -p --path" -- "$cur"
+        SPACK_COMPREPLY="-h --help -n --no-checksum -p --path"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_test () {
+_spack_test() {
     if $list_options
     then
-        compgen -W "-h --help -H --pytest-help --extension -l --list -L --list-long -N --list-names -s -k --showlocals" -- "$cur"
+        SPACK_COMPREPLY="-h --help -H --pytest-help -l --list -L --list-long -N --list-names --extension -s -k --showlocals"
     else
-        compgen -W "$(_tests)" -- "$cur"
+        _tests
     fi
 }
 
-_spack_uninstall () {
+_spack_uninstall() {
     if $list_options
     then
-        compgen -W "-h --help -f --force -R --dependents -y --yes-to-all -a --all" -- "$cur"
+        SPACK_COMPREPLY="-h --help -f --force -R --dependents -y --yes-to-all -a --all"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_unload () {
+_spack_unload() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "$(_installed_packages)" -- "$cur"
+        _installed_packages
     fi
 }
 
-_spack_upload_s3 () {
+_spack_upload_s3() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "spec index" -- "$cur"
+        SPACK_COMPREPLY="spec index"
     fi
 }
 
-_spack_upload_s3_spec () {
-    compgen -W "-h --help -s --spec -y --spec-yaml -b --base-dir -e --endpoint-url" -- "$cur"
+_spack_upload_s3_spec() {
+    SPACK_COMPREPLY="-h --help -s --spec -y --spec-yaml -b --base-dir -e --endpoint-url"
 }
 
-_spack_upload_s3_index () {
-    compgen -W "-h --help -e --endpoint-url" -- "$cur"
+_spack_upload_s3_index() {
+    SPACK_COMPREPLY="-h --help -e --endpoint-url"
 }
 
-_spack_url () {
+_spack_url() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help"
     else
-        compgen -W "parse list summary stats" -- "$cur"
+        SPACK_COMPREPLY="parse list summary stats"
     fi
 }
 
-_spack_url_parse () {
+_spack_url_parse() {
     if $list_options
     then
-        compgen -W "-h --help -s --spider" -- "$cur"
+        SPACK_COMPREPLY="-h --help -s --spider"
+    else
+        SPACK_COMPREPLY=""
     fi
 }
 
-_spack_url_list () {
-    compgen -W "-h --help -c --color -e --extrapolation -n --incorrect-name -N --correct-name -v --incorrect-version -V --correct-version" -- "$cur"
+_spack_url_list() {
+    SPACK_COMPREPLY="-h --help -c --color -e --extrapolation -n --incorrect-name -N --correct-name -v --incorrect-version -V --correct-version"
 }
 
-_spack_url_summary () {
-    compgen -W "-h --help" -- "$cur"
+_spack_url_summary() {
+    SPACK_COMPREPLY="-h --help"
 }
 
-_spack_url_stats () {
-    compgen -W "-h --help" -- "$cur"
+_spack_url_stats() {
+    SPACK_COMPREPLY="-h --help"
 }
 
-_spack_verify () {
+_spack_verify() {
     if $list_options
     then
-        compgen -W "-h --help -l --local -j --json -a --all -s --specs -f --files" -- "$cur"
+        SPACK_COMPREPLY="-h --help -l --local -j --json -a --all -s --specs -f --files"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_versions () {
+_spack_versions() {
     if $list_options
     then
-        compgen -W "-h --help -s --safe-only" -- "$cur"
+        SPACK_COMPREPLY="-h --help -s --safe-only"
     else
-        compgen -W "$(_all_packages)" -- "$cur"
+        _all_packages
     fi
 }
 
-_spack_view () {
+_spack_view() {
     if $list_options
     then
-        compgen -W "-h --help -v --verbose -e --exclude -d --dependencies" -- "$cur"
+        SPACK_COMPREPLY="-h --help -v --verbose -e --exclude -d --dependencies"
     else
-        compgen -W "symlink add soft hardlink hard remove rm statlink status check" -- "$cur"
+        SPACK_COMPREPLY="symlink add soft hardlink hard remove rm statlink status check"
     fi
 }
 
-_spack_view_symlink () {
+_spack_view_symlink() {
     if $list_options
     then
-        compgen -W "-h --help --projection-file -i --ignore-conflicts" -- "$cur"
+        SPACK_COMPREPLY="-h --help --projection-file -i --ignore-conflicts"
+    else
+        _all_packages
     fi
 }
 
-_spack_view_add () {
-    # Alias for `spack view symlink`
-    _spack_view_symlink
-}
-
-_spack_view_soft () {
-    # Alias for `spack view symlink`
-    _spack_view_symlink
-}
-
-_spack_view_hardlink () {
+_spack_view_add() {
     if $list_options
     then
-        compgen -W "-h --help --projection-file -i --ignore-conflicts" -- "$cur"
+        SPACK_COMPREPLY="-h --help --projection-file -i --ignore-conflicts"
+    else
+        _all_packages
     fi
 }
 
-_spack_view_hard () {
-    # Alias for `spack view hardlink`
-    _spack_view_hardlink
-}
-
-_spack_view_remove () {
+_spack_view_soft() {
     if $list_options
     then
-        compgen -W "-h --help --no-remove-dependents -a --all" -- "$cur"
+        SPACK_COMPREPLY="-h --help --projection-file -i --ignore-conflicts"
+    else
+        _all_packages
     fi
 }
 
-_spack_view_rm () {
-    # Alias for `spack view remove`
-    _spack_view_remove
-}
-
-_spack_view_statlink () {
+_spack_view_hardlink() {
     if $list_options
     then
-        compgen -W "-h --help" -- "$cur"
+        SPACK_COMPREPLY="-h --help --projection-file -i --ignore-conflicts"
+    else
+        _all_packages
     fi
 }
 
-_spack_view_status () {
-    # Alias for `spack view statlink`
-    _spack_view_statlink
-}
-
-_spack_view_check () {
-    # Alias for `spack view statlink`
-    _spack_view_statlink
-}
-
-# Helper functions for subcommands
-
-_subcommands () {
-    spack commands
-}
-
-_all_packages () {
-    spack list
-}
-
-_all_resource_hashes () {
-    spack resource list --only-hashes
-}
-
-_installed_packages () {
-    spack --color=never find --no-groups
-}
-
-_installed_compilers () {
-    spack compilers | egrep -v "^(-|=)"
-}
-
-_providers () {
-    spack providers
-}
-
-_mirrors () {
-    spack mirror list | awk '{print $1}'
-}
-
-_repos () {
-    spack repo list | awk '{print $1}'
+_spack_view_hard() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help --projection-file -i --ignore-conflicts"
+    else
+        _all_packages
+    fi
 }
 
-_tests () {
-    spack test -l
+_spack_view_remove() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help --no-remove-dependents -a --all"
+    else
+        _all_packages
+    fi
 }
 
-_environments () {
-    spack env list
+_spack_view_rm() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help --no-remove-dependents -a --all"
+    else
+        _all_packages
+    fi
 }
 
-_keys () {
-    spack gpg list
+_spack_view_statlink() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help"
+    else
+        _all_packages
+    fi
 }
 
-# Testing functions
-
-_test_vars () {
-    echo "-----------------------------------------------------"             >> temp
-    echo "Full line:                '$COMP_LINE'"                            >> temp
-    echo                                                                     >> temp
-    echo "Word list w/ flags:       $(_pretty_print COMP_WORDS[@])"          >> temp
-    echo "# words w/ flags:         '${#COMP_WORDS[@]}'"                     >> temp
-    echo "Cursor index w/ flags:    '$COMP_CWORD'"                           >> temp
-    echo                                                                     >> temp
-    echo "Word list w/out flags:    $(_pretty_print COMP_WORDS_NO_FLAGS[@])" >> temp
-    echo "# words w/out flags:      '${#COMP_WORDS_NO_FLAGS[@]}'"            >> temp
-    echo "Cursor index w/out flags: '$COMP_CWORD_NO_FLAGS'"                  >> temp
-    echo                                                                     >> temp
-    echo "Subfunction:              '$subfunction'"                          >> temp
+_spack_view_status() {
     if $list_options
     then
-        echo "List options:             'True'"  >> temp
+        SPACK_COMPREPLY="-h --help"
     else
-        echo "List options:             'False'" >> temp
+        _all_packages
     fi
-    echo "Current word:             '$cur'"  >> temp
-    echo "Previous word:            '$prev'" >> temp
 }
 
-# Pretty-prints one or more arrays
-# Syntax: _pretty_print array1[@] ...
-_pretty_print () {
-    for arg in $@
-    do
-        local array=("${!arg}")
-        echo -n "$arg: ["
-        printf   "'%s'" "${array[0]}"
-        printf ", '%s'" "${array[@]:1}"
-        echo "]"
-    done
+_spack_view_check() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help"
+    else
+        _all_packages
+    fi
 }
-
-complete -o default -F _bash_completion_spack spack