diff --git a/doc/manpages/qubes-prefs.rst b/doc/manpages/qubes-prefs.rst index 03f7acc2..877e9df9 100644 --- a/doc/manpages/qubes-prefs.rst +++ b/doc/manpages/qubes-prefs.rst @@ -6,7 +6,7 @@ Synopsis -------- -:command:`qubes-prefs` [-h] [--xml *XMLFILE*] [--verbose] [--quiet] [--force-root] [--help-properties] [*PROPERTY* [*VALUE*\|--delete]] +:command:`qubes-prefs` [-h] [--verbose] [--quiet] [--force-root] [--help-properties] [*PROPERTY* [*VALUE*\|--delete]] Options ------- @@ -19,10 +19,6 @@ Options List available properties with short descriptions and exit. -.. option:: --qubesxml=XMLFILE - - Qubes OS store file. - .. option:: --verbose, -v Increase verbosity. diff --git a/doc/manpages/qvm-create.rst b/doc/manpages/qvm-create.rst index c287b4b6..ed008e97 100644 --- a/doc/manpages/qvm-create.rst +++ b/doc/manpages/qvm-create.rst @@ -6,7 +6,7 @@ Synopsis -------- -:command:`qvm-create` [-h] [--qubesxml *XMLFILE*] [--force-root] [--class *CLS*] [--property *NAME*=*VALUE*] [--template *VALUE*] [--label *VALUE*] [--root-copy-from *FILENAME* | --root-move-from *FILENAME*] *VMNAME* +:command:`qvm-create` [-h] [--verbose] [--quiet] [--force-root] [--class *CLS*] [--property *NAME*=*VALUE*] [--pool *POOL_NAME:VOLUME_NAME*] [--template *VALUE*] --label *VALUE* [--root-copy-from *FILENAME* | --root-move-from *FILENAME*] *VMNAME* Options ------- @@ -15,9 +15,13 @@ Options show help message and exit -.. option:: --qubesxml=XMLFILE +.. option:: --verbose, -v - Qubes OS store file + Increase verbosity. + +.. option:: --quiet, -q + + Decrease verbosity. .. option:: --force-root diff --git a/doc/manpages/qvm-ls.rst b/doc/manpages/qvm-ls.rst index 9c1a4d4a..bdc65157 100644 --- a/doc/manpages/qvm-ls.rst +++ b/doc/manpages/qvm-ls.rst @@ -6,8 +6,7 @@ Synopsis -------- -:command:`qvm-ls` [*options*] - +:command:`qvm-ls` [-h] [--verbose] [--quiet] [--help-columns] [--help-formats] [--format *FORMAT* | --fields *FIELD*,...] Options ------- @@ -35,10 +34,13 @@ Options :option:`--format`. All columns along with short descriptions can be listed with :option:`--help-columns`. -.. option:: --qubesxml=XMLFILE +.. option:: --verbose, -v - Qubes store file + Increase verbosity. +.. option:: --quiet, -q + + Decrease verbosity. Authors ------- diff --git a/doc/manpages/qvm-prefs.rst b/doc/manpages/qvm-prefs.rst index f3b9d01b..ce22b399 100644 --- a/doc/manpages/qvm-prefs.rst +++ b/doc/manpages/qvm-prefs.rst @@ -6,7 +6,7 @@ Synopsis -------- -:command:`qvm-prefs` qvm-prefs [-h] [--xml *XMLFILE*] [--verbose] [--quiet] [--force-root] [--help-properties] *VMNAME* [*PROPERTY* [*VALUE*\|--delete]] +:command:`qvm-prefs` qvm-prefs [-h] [--verbose] [--quiet] [--force-root] [--help-properties] *VMNAME* [*PROPERTY* [*VALUE* \| --delete \| --default ]] Options ------- @@ -19,10 +19,6 @@ Options List available properties with short descriptions and exit. -.. option:: --qubesxml=XMLFILE - - Qubes OS store file. - .. option:: --verbose, -v Increase verbosity. diff --git a/doc/manpages/qvm-start.rst b/doc/manpages/qvm-start.rst index c2c1738b..f797cdbf 100644 --- a/doc/manpages/qvm-start.rst +++ b/doc/manpages/qvm-start.rst @@ -24,10 +24,6 @@ Options Show help message and exit. -.. option:: --qubesxml=XMLFILE - - Use another :file:`qubes.xml` file. - .. option:: --verbose, -v Increase verbosity. @@ -65,6 +61,10 @@ Options Do actions necessary when preparing DVM image. +.. option:: --skip-if-running + + Do not fail if the qube is already runnning + .. option:: --no-start-guid Do not start GUI daemon. diff --git a/qubes/dochelpers.py b/qubes/dochelpers.py index 054c70aa..1e0259ec 100644 --- a/qubes/dochelpers.py +++ b/qubes/dochelpers.py @@ -28,6 +28,7 @@ This module contains classes and functions which help to maintain documentation, particularly our custom Sphinx extension. ''' +import argparse import csv import os import posixpath @@ -40,12 +41,14 @@ import docutils.nodes import docutils.parsers.rst import docutils.parsers.rst.roles import docutils.statemachine +import qubes.tools import sphinx import sphinx.errors import sphinx.locale import sphinx.util.docfields -import qubes.tools +SUBCOMMANDS_TITLE = 'COMMANDS' +OPTIONS_TITLE = 'OPTIONS' def fetch_ticket_info(uri): @@ -209,39 +212,24 @@ def prepare_manpage(command): return stream.getvalue() -class ArgumentCheckVisitor(docutils.nodes.SparseNodeVisitor): - def __init__(self, app, command, document): +class OptionsCheckVisitor(docutils.nodes.SparseNodeVisitor): + ''' Checks if the visited option nodes and the specified args are in sync. + ''' + def __init__(self, command, args, document): + assert isinstance(args, set) docutils.nodes.SparseNodeVisitor.__init__(self, document) - - self.app = app self.command = command - self.args = set() - - try: - parser = qubes.tools.get_parser_for_command(command) - except ImportError: - self.app.warn('cannot import module for command') - self.command = None - return - except AttributeError: - raise sphinx.errors.SphinxError('cannot find parser in module') - - # pylint: disable=protected-access - for action in parser._actions: - self.args.update(action.option_strings) - - - # pylint: disable=no-self-use,unused-argument + self.args = args def visit_desc(self, node): + ''' Skips all but 'option' elements ''' + # pylint: disable=no-self-use if not node.get('desctype', None) == 'option': raise docutils.nodes.SkipChildren def visit_desc_name(self, node): - if self.command is None: - return - + ''' Checks if the option is defined `self.args` ''' if not isinstance(node[0], docutils.nodes.Text): raise sphinx.errors.SphinxError('first child should be Text') @@ -252,18 +240,149 @@ class ArgumentCheckVisitor(docutils.nodes.SparseNodeVisitor): raise sphinx.errors.SphinxError( 'No such argument for {!r}: {!r}'.format(self.command, arg)) + def check_undocumented_arguments(self, ignored_options=set()): + ''' Call this to check if any undocumented arguments are left. - def depart_document(self, node): - if self.args: + While the documentation talks about a + 'SparseNodeVisitor.depart_document()' function, this function does + not exists. (For details see implementation of + :py:method:`NodeVisitor.dispatch_departure()`) So we need to + manually call this. + ''' + left_over_args = self.args - ignored_options + if left_over_args: raise sphinx.errors.SphinxError( - 'Undocumented arguments: {!r}'.format( - ', '.join(sorted(self.args)))) + 'Undocumented arguments for command {!r}: {!r}'.format( + self.command, ', '.join(sorted(left_over_args)))) +class CommandCheckVisitor(docutils.nodes.SparseNodeVisitor): + ''' Checks if the visited sub command section nodes and the specified sub + command args are in sync. + ''' + + def __init__(self, command, sub_commands, document): + docutils.nodes.SparseNodeVisitor.__init__(self, document) + self.command = command + self.sub_commands = sub_commands + + def visit_section(self, node): + ''' Checks if the visited sub-command section nodes exists and it + options are in sync. + + Uses :py:class:`OptionsCheckVisitor` for checking + sub-commands options + ''' + # pylint: disable=no-self-use + title = str(node[0][0]) + if title.upper() == SUBCOMMANDS_TITLE: + return + + sub_cmd = self.command + ' ' + title + + try: + args = self.sub_commands[title] + options_visitor = OptionsCheckVisitor(sub_cmd, args, self.document) + node.walkabout(options_visitor) + options_visitor.check_undocumented_arguments( + {'--help', '--quiet', '--verbose', '-h', '-q', '-v'}) + del self.sub_commands[title] + except KeyError: + raise sphinx.errors.SphinxError( + 'No such sub-command {!r}'.format(sub_cmd)) + + def visit_Text(self, node): + ''' If the visited text node starts with 'alias: ', all the provided + comma separted alias in this node, are removed from + `self.sub_commands` + ''' + # pylint: disable=invalid-name + text = str(node).strip() + if text.startswith('aliases:'): + aliases = {a.strip() for a in text.split('aliases:')[1].split(',')} + for alias in aliases: + assert alias in self.sub_commands + del self.sub_commands[alias] + + + def check_undocumented_sub_commands(self): + ''' Call this to check if any undocumented sub_commands are left. + + While the documentation talks about a + 'SparseNodeVisitor.depart_document()' function, this function does + not exists. (For details see implementation of + :py:method:`NodeVisitor.dispatch_departure()`) So we need to + manually call this. + ''' + if self.sub_commands: + raise sphinx.errors.SphinxError( + 'Undocumented commands for {!r}: {!r}'.format( + self.command, ', '.join(sorted(self.sub_commands.keys())))) + + +class ManpageCheckVisitor(docutils.nodes.SparseNodeVisitor): + ''' Checks if the sub-commands and options specified in the 'COMMAND' and + 'OPTIONS' (case insensitve) sections in sync the command parser. + ''' + def __init__(self, app, command, document): + docutils.nodes.SparseNodeVisitor.__init__(self, document) + try: + parser = qubes.tools.get_parser_for_command(command) + except ImportError: + app.warn('cannot import module for command') + self.parser = None + return + except AttributeError: + raise sphinx.errors.SphinxError('cannot find parser in module') + + self.command = command + self.parser = parser + self.options = set() + self.sub_commands = {} + self.app = app + + # pylint: disable=protected-access + for action in parser._actions: + if action.help == argparse.SUPPRESS: + continue + + if issubclass(action.__class__, + qubes.tools.AliasedSubParsersAction): + for cmd, cmd_parser in action._name_parser_map.items(): + self.sub_commands[cmd] = set() + for sub_action in cmd_parser._actions: + if sub_action.help != argparse.SUPPRESS: + self.sub_commands[cmd].update( + sub_action.option_strings) + else: + self.options.update(action.option_strings) + + def visit_section(self, node): + ''' If section title is OPTIONS or COMMANDS dispatch the apropriate + `NodeVisitor`. + ''' + if self.parser is None: + return + + section_title = str(node[0][0]).upper() + if section_title == OPTIONS_TITLE: + options_visitor = OptionsCheckVisitor(self.command, self.options, + self.document) + node.walkabout(options_visitor) + options_visitor.check_undocumented_arguments() + elif section_title == SUBCOMMANDS_TITLE: + sub_cmd_visitor = CommandCheckVisitor( + self.command, self.sub_commands, self.document) + node.walkabout(sub_cmd_visitor) + sub_cmd_visitor.check_undocumented_sub_commands() + def check_man_args(app, doctree, docname): + ''' Checks the manpage for undocumented or obsolete sub-commands and + options. + ''' command = os.path.split(docname)[1] app.info('Checking arguments for {!r}'.format(command)) - doctree.walk(ArgumentCheckVisitor(app, command, doctree)) + doctree.walk(ManpageCheckVisitor(app, command, doctree)) # diff --git a/qubes/tools/__init__.py b/qubes/tools/__init__.py index b66299e3..81d7b27e 100644 --- a/qubes/tools/__init__.py +++ b/qubes/tools/__init__.py @@ -319,6 +319,45 @@ class QubesArgumentParser(argparse.ArgumentParser): print(*args, file=sys.stderr, **kwargs) +class AliasedSubParsersAction(argparse._SubParsersAction): + # source https://gist.github.com/sampsyo/471779 + # pylint: disable=protected-access,too-few-public-methods + class _AliasedPseudoAction(argparse.Action): + # pylint: disable=redefined-builtin + def __init__(self, name, aliases, help): + dest = name + if aliases: + dest += ' (%s)' % ','.join(aliases) + sup = super(AliasedSubParsersAction._AliasedPseudoAction, self) + sup.__init__(option_strings=[], dest=dest, help=help) + + def __call__(self, **kwargs): + super(AliasedSubParsersAction._AliasedPseudoAction, self).__call__( + **kwargs) + + def add_parser(self, name, **kwargs): + if 'aliases' in kwargs: + aliases = kwargs['aliases'] + del kwargs['aliases'] + else: + aliases = [] + + local_parser = super(AliasedSubParsersAction, self).add_parser( + name, **kwargs) + + # Make the aliases work. + for alias in aliases: + self._name_parser_map[alias] = local_parser + # Make the help text reflect them, first removing old help entry. + if 'help' in kwargs: + self._choices_actions.pop() + pseudo_action = self._AliasedPseudoAction(name, aliases, + kwargs.pop('help')) + self._choices_actions.append(pseudo_action) + + return local_parser + + def get_parser_for_command(command): '''Get parser for given qvm-tool.