diff --git a/doc/index.rst b/doc/index.rst index c5d3eb8b..f04ef7a3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -19,6 +19,7 @@ manpages and API documentation. For primary user documentation, see qubes-plugins qubes-ext qubes-log + qubes-tools/index qubes-tests qubes-dochelpers diff --git a/doc/qubes-tools/index.rst b/doc/qubes-tools/index.rst new file mode 100644 index 00000000..185fe896 --- /dev/null +++ b/doc/qubes-tools/index.rst @@ -0,0 +1,32 @@ +:py:mod:`qubes.tools` -- Command line utilities +=============================================== + +Those are Python modules that house actual functionality of CLI tools -- the +files installed in :file:`/usr/bin` only import these modules and run ``main()`` +function. + +The modules should make available for import theirs command line parsers +(instances of :py:class:`argparse.ArgumentParser`) as either ``.parser`` +attribute or function ``get_parser()``, which returns parser. Manual page will +be automatically checked during generation if its "Options" section contains all +options from this parser (and only those). + + +Module contents +--------------- + +.. automodule:: qubes.tools + :members: + :show-inheritance: + + +All CLI tools +------------- + +.. toctree:: + :maxdepth: 1 + :glob: + + * + +.. vim: ts=3 sw=3 et tw=80 diff --git a/doc/qubes-tools/qvm_ls.rst b/doc/qubes-tools/qvm_ls.rst new file mode 100644 index 00000000..730c8775 --- /dev/null +++ b/doc/qubes-tools/qvm_ls.rst @@ -0,0 +1,8 @@ +:py:mod:`qubes.tools.qvm_ls` -- VM listing +========================================== + +.. automodule:: qubes.tools.qvm_ls + :members: + :show-inheritance: + +.. vim: ts=3 sw=3 et diff --git a/doc/skel-manpage.py b/doc/skel-manpage.py new file mode 100755 index 00000000..afd62912 --- /dev/null +++ b/doc/skel-manpage.py @@ -0,0 +1,20 @@ +#!/usr/bin/python + +import os +import sys +sys.path.insert(0, os.path.abspath('../')) + +import argparse +import qubes.dochelpers + +parser = argparse.ArgumentParser(description='prepare new manpage for command') +parser.add_argument('command', metavar='COMMAND', + help='program\'s command name; this should translate to ' + 'qubes.tools.') + +def main(): + args = parser.parse_args() + sys.stdout.write(qubes.dochelpers.prepare_manpage(args.command)) + +if __name__ == '__main__': + main() diff --git a/qubes/__init__.py b/qubes/__init__.py index c0bf82b6..0c9ec97d 100644 --- a/qubes/__init__.py +++ b/qubes/__init__.py @@ -81,6 +81,9 @@ class QubesException(Exception): pass +import qubes.events + + class VMMConnection(object): '''Connection to Virtual Machine Manager (libvirt)''' diff --git a/qubes/dochelpers.py b/qubes/dochelpers.py index 2120dfb3..054c70aa 100644 --- a/qubes/dochelpers.py +++ b/qubes/dochelpers.py @@ -29,8 +29,10 @@ particularly our custom Sphinx extension. ''' import csv +import os import posixpath import re +import StringIO import urllib2 import docutils @@ -39,9 +41,13 @@ import docutils.parsers.rst import docutils.parsers.rst.roles import docutils.statemachine import sphinx +import sphinx.errors import sphinx.locale import sphinx.util.docfields +import qubes.tools + + def fetch_ticket_info(uri): '''Fetch info about particular trac ticket given @@ -75,7 +81,7 @@ def ticket(name, rawtext, text, lineno, inliner, options=None, content=None): options = {} ticketno = text.lstrip('#') - if not ticket.isdigit(): + if not ticketno.isdigit(): msg = inliner.reporter.error( 'Invalid ticket identificator: {!r}'.format(text), line=lineno) prb = inliner.problematic(rawtext, rawtext, msg) @@ -147,6 +153,119 @@ class VersionCheck(docutils.parsers.rst.Directive): return [node] +def make_rst_section(heading, char): + return '{}\n{}\n\n'.format(heading, char[0] * len(heading)) + + +def prepare_manpage(command): + parser = qubes.tools.get_parser_for_command(command) + stream = StringIO.StringIO() + stream.write('.. program:: {}\n\n'.format(command)) + stream.write(make_rst_section( + ':program:`{}` -- {}'.format(command, parser.description), '=')) + stream.write('''.. warning:: + + This page was autogenerated from command-line parser. It shouldn't be 1:1 + conversion, because it would add little value. Please revise it and add + more descriptive help, which normally won't fit in standard ``--help`` + option. + + After rewrite, please remove this admonition.\n\n''') + + stream.write(make_rst_section('Synopsis', '-')) + usage = ' '.join(parser.format_usage().strip().split()) + if usage.startswith('usage: '): + usage = usage[len('usage: '):] + + # replace METAVARS with *METAVARS* + usage = re.sub(r'\b([A-Z]{2,})\b', r'*\1*', usage) + + stream.write(':command:`{}` {}\n\n'.format(command, usage)) + + stream.write(make_rst_section('Options', '-')) + + for action in parser._actions: # pylint: disable=protected-access + stream.write('.. option:: ') + if action.metavar: + stream.write(', '.join('{}{}{}'.format( + option, + '=' if option.startswith('--') else ' ', + action.metavar) + for option in sorted(action.option_strings))) + else: + stream.write(', '.join(sorted(action.option_strings))) + stream.write('\n\n {}\n\n'.format(action.help)) + + stream.write(make_rst_section('Authors', '-')) + stream.write('''\ +| Joanna Rutkowska +| Rafal Wojtczuk +| Marek Marczykowski +| Wojtek Porczyk + +.. vim: ts=3 sw=3 et tw=80 +''') + + return stream.getvalue() + + +class ArgumentCheckVisitor(docutils.nodes.SparseNodeVisitor): + def __init__(self, app, command, document): + 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 + + def visit_desc(self, node): + if not node.get('desctype', None) == 'option': + raise docutils.nodes.SkipChildren + + + def visit_desc_name(self, node): + if self.command is None: + return + + if not isinstance(node[0], docutils.nodes.Text): + raise sphinx.errors.SphinxError('first child should be Text') + + arg = str(node[0]) + try: + self.args.remove(arg) + except KeyError: + raise sphinx.errors.SphinxError( + 'No such argument for {!r}: {!r}'.format(self.command, arg)) + + + def depart_document(self, node): + if self.args: + raise sphinx.errors.SphinxError( + 'Undocumented arguments: {!r}'.format( + ', '.join(sorted(self.args)))) + + +def check_man_args(app, doctree, docname): + command = os.path.split(docname)[1] + app.info('Checking arguments for {!r}'.format(command)) + doctree.walk(ArgumentCheckVisitor(app, command, doctree)) + + # # this is lifted from sphinx' own conf.py # @@ -172,10 +291,19 @@ def parse_event(env, sig, signode): # end of codelifting # + +def break_to_pdb(app, *dummy): + if not app.config.break_to_pdb: + return + import pdb + pdb.set_trace() + + def setup(app): app.add_role('ticket', ticket) app.add_config_value('ticket_base_uri', 'https://wiki.qubes-os.org/ticket/', 'env') + app.add_config_value('break_to_pdb', False, 'env') app.add_node(versioncheck, html=(visit, depart), man=(visit, depart)) @@ -186,5 +314,8 @@ def setup(app): app.add_object_type('event', 'event', 'pair: %s; event', parse_event, doc_field_types=[fdesc]) + app.connect('doctree-resolved', break_to_pdb) + app.connect('doctree-resolved', check_man_args) + # vim: ts=4 sw=4 et diff --git a/qubes/tools/__init__.py b/qubes/tools/__init__.py index 388083ed..4143f902 100644 --- a/qubes/tools/__init__.py +++ b/qubes/tools/__init__.py @@ -1 +1,50 @@ -# pylint: skip-file +#!/usr/bin/python2 -O +# vim: fileencoding=utf-8 + +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2015 Joanna Rutkowska +# Copyright (C) 2015 Wojtek Porczyk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +'''Qubes' command line tools +''' + +import importlib + +def get_parser_for_command(command): + '''Get parser for given qvm-tool. + + :param str command: command name + :rtype: argparse.ArgumentParser + :raises ImportError: when command's module is not found + :raises AttributeError: when parser was not found + ''' + + module = importlib.import_module( + '.' + command.replace('-', '_'), 'qubes.tools') + + try: + parser = module.parser + except AttributeError: + try: + parser = module.get_parser() + except AttributeError: + raise AttributeError('cannot find parser in module') + + return parser