# Copyright 2013-2022 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)
from __future__ import print_function
import argparse
import copy
import os
import re
import sys
import llnl.util.filesystem as fs
import llnl.util.tty as tty
from llnl.util.argparsewriter import (
ArgparseCompletionWriter,
ArgparseRstWriter,
ArgparseWriter,
)
from llnl.util.tty.colify import colify
import spack.cmd
import spack.main
import spack.paths
from spack.main import section_descriptions
description = "list available spack commands"
section = "developer"
level = "long"
#: list of command formatters
formatters = {}
#: standard arguments for updating completion scripts
#: we iterate through these when called with --update-completion
update_completion_args = {
"bash": {
"aliases": True,
"format": "bash",
"header": os.path.join(
spack.paths.share_path, "bash", "spack-completion.in"),
"update": os.path.join(
spack.paths.share_path, "spack-completion.bash"),
},
}
[docs]def setup_parser(subparser):
subparser.add_argument(
"--update-completion", action='store_true', default=False,
help="regenerate spack's tab completion scripts")
subparser.add_argument(
'-a', '--aliases', action='store_true', default=False,
help='include command aliases')
subparser.add_argument(
'--format', default='names', choices=formatters,
help='format to be used to print the output (default: names)')
subparser.add_argument(
'--header', metavar='FILE', default=None, action='store',
help='prepend contents of FILE to the output (useful for rst format)')
subparser.add_argument(
'--update', metavar='FILE', default=None, action='store',
help='write output to the specified file, if any command is newer')
subparser.add_argument(
'rst_files', nargs=argparse.REMAINDER,
help='list of rst files to search for `_cmd-spack-<cmd>` cross-refs')
[docs]class SpackArgparseRstWriter(ArgparseRstWriter):
"""RST writer tailored for spack documentation."""
def __init__(self, prog, out=None, aliases=False,
documented_commands=[],
rst_levels=['-', '-', '^', '~', ':', '`']):
out = sys.stdout if out is None else out
super(SpackArgparseRstWriter, self).__init__(
prog, out, aliases, rst_levels)
self.documented = documented_commands
[docs] def usage(self, *args):
string = super(SpackArgparseRstWriter, self).usage(*args)
cmd = self.parser.prog.replace(' ', '-')
if cmd in self.documented:
string += '\n:ref:`More documentation <cmd-{0}>`\n'.format(cmd)
return string
[docs]class SubcommandWriter(ArgparseWriter):
_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': '_unit_tests',
}
[docs]class BashCompletionWriter(ArgparseCompletionWriter):
"""Write argparse output as bash programmable tab completion."""
[docs] 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))
[docs] 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=""'
[docs] def optionals(self, optionals):
return 'SPACK_COMPREPLY="{0}"'.format(' '.join(optionals))
[docs] def subcommands(self, subcommands):
return 'SPACK_COMPREPLY="{0}"'.format(' '.join(subcommands))
[docs]@formatter
def subcommands(args, out):
parser = spack.main.make_argument_parser()
spack.main.add_all_commands(parser)
writer = SubcommandWriter(parser.prog, out, args.aliases)
writer.write(parser)
[docs]def rst_index(out):
out.write('\n')
index = spack.main.index_commands()
sections = index['long']
dmax = max(len(section_descriptions.get(s, s)) for s in sections) + 2
cmax = max(len(c) for _, c in sections.items()) + 60
row = "%s %s\n" % ('=' * dmax, '=' * cmax)
line = '%%-%ds %%s\n' % dmax
out.write(row)
out.write(line % (" Category ", " Commands "))
out.write(row)
for section, commands in sorted(sections.items()):
description = section_descriptions.get(section, section)
for i, cmd in enumerate(sorted(commands)):
description = description.capitalize() if i == 0 else ''
ref = ':ref:`%s <spack-%s>`' % (cmd, cmd)
comma = ',' if i != len(commands) - 1 else ''
bar = '| ' if i % 8 == 0 else ' '
out.write(line % (description, bar + ref + comma))
out.write(row)
[docs]@formatter
def rst(args, out):
# create a parser with all commands
parser = spack.main.make_argument_parser()
spack.main.add_all_commands(parser)
# extract cross-refs of the form `_cmd-spack-<cmd>:` from rst files
documented_commands = set()
for filename in args.rst_files:
with open(filename) as f:
for line in f:
match = re.match(r'\.\. _cmd-(spack-.*):', line)
if match:
documented_commands.add(match.group(1).strip())
# print an index to each command
rst_index(out)
out.write('\n')
# print sections for each command and subcommand
writer = SpackArgparseRstWriter(
parser.prog, out, args.aliases, documented_commands)
writer.write(parser)
[docs]@formatter
def names(args, out):
commands = copy.copy(spack.cmd.all_commands())
if args.aliases:
commands.extend(spack.main.aliases.keys())
colify(commands, output=out)
[docs]@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 _commands(parser, args):
"""This is the 'regular' command, which can be called multiple times.
See ``commands()`` below for ``--update-completion`` handling.
"""
formatter = formatters[args.format]
# check header first so we don't open out files unnecessarily
if args.header and not os.path.exists(args.header):
tty.die("No such file: '%s'" % args.header)
if args.update:
tty.msg('Updating file: %s' % args.update)
with open(args.update, 'w') as f:
prepend_header(args, f)
formatter(args, f)
if args.update_completion:
fs.set_executable(args.update)
else:
prepend_header(args, sys.stdout)
formatter(args, sys.stdout)
[docs]def update_completion(parser, args):
"""Iterate through the shells and update the standard completion files.
This is a convenience method to avoid calling this command many
times, and to simplify completion update for developers.
"""
for shell, shell_args in update_completion_args.items():
for attr, value in shell_args.items():
setattr(args, attr, value)
_commands(parser, args)
[docs]def commands(parser, args):
if args.update_completion:
if args.format != 'names' or any([
args.aliases, args.update, args.header
]):
tty.die("--update-completion can only be specified alone.")
# this runs the command multiple times with different arguments
return update_completion(parser, args)
else:
# run commands normally
return _commands(parser, args)