diff options
author | Nguyễn Gia Phong <cnx@loang.net> | 2025-06-15 21:53:53 +0900 |
---|---|---|
committer | Nguyễn Gia Phong <cnx@loang.net> | 2025-06-15 21:53:53 +0900 |
commit | c3d356c8b80659468a0a7429636a128c156509ae (patch) | |
tree | 8d7af71b623e1cfb360ee935f809411059e0ca76 | |
parent | 637e73f023107c142c1eecc187c18a5581c10794 (diff) | |
download | scadere-c3d356c8b80659468a0a7429636a128c156509ae.tar.gz |
Add examples to help and man pages
-rw-r--r-- | README.md | 15 | ||||
-rw-r--r-- | src/scadere/__init__.py | 34 | ||||
-rw-r--r-- | src/scadere/check.py | 21 | ||||
-rw-r--r-- | src/scadere/listen.py | 34 | ||||
-rw-r--r-- | tst/test_help.py | 24 |
5 files changed, 106 insertions, 22 deletions
diff --git a/README.md b/README.md index 9355b66..d1f40f4 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,18 @@ It is recommended to run `scadere-check` as a cron job. ```console $ scadere-listen --help -Usage: scadere-listen [-h] [-v] INPUT URL [[HOST][:PORT]] +Usage: scadere-listen [-h] [-v] PATH URL [[HOST][:PORT]] -Serve the TLS certificate expiration feed from INPUT file -for base URL at HOST:PORT, where HOST defaults to localhost -and PORT is selected randomly if not specified. +Serve at URL Atom feeds for TLS certificate renewal reminder. +It is possible for clients to filter domains +using one or more "domain" URL queries. + +The certificate information is read from the file at PATH, +which is generated by scadere-check(1). + +The server listens for TCP connections coming to HOST:PORT, +where HOST defaults to localhost and PORT is selected randomly +if not specified. Options: -h, --help show this help message and exit diff --git a/src/scadere/__init__.py b/src/scadere/__init__.py index ada0f29..f2b41bd 100644 --- a/src/scadere/__init__.py +++ b/src/scadere/__init__.py @@ -18,10 +18,15 @@ from argparse import HelpFormatter, ONE_OR_MORE -__all__ = ['__version__', 'GNUHelpFormatter', 'NetLoc'] +__all__ = ['__version__', 'GNUHelpFormatter', 'NetLoc', 'format_examples'] __version__ = '0.1.0' +EXAMPLE_PREFIX = ' ' * 2 +# help2man's implementation detail +EXAMPLE_DESCRIPTION_PREFIX = ' ' * 20 + + class GNUHelpFormatter(HelpFormatter): """Help formatter for ArgumentParser following GNU Coding Standards.""" @@ -33,6 +38,24 @@ class GNUHelpFormatter(HelpFormatter): """Substitute 'Options:' for 'options:'.""" super().start_section(heading.capitalize()) + def _fill_text(self, text, width, indent): + """Preserve examples' formatting.""" + desc_position = max(len(EXAMPLE_DESCRIPTION_PREFIX), + min(self._action_max_length+2, + self._max_help_position)) + desc_width = width - desc_position + desc_indent = indent + ' '*desc_position + example_indent = indent + EXAMPLE_PREFIX + parts = [] + for line in text.splitlines(): + if line.startswith(EXAMPLE_DESCRIPTION_PREFIX): + parts.append(super()._fill_text(line, desc_width, desc_indent)) + elif line.startswith(EXAMPLE_PREFIX): + parts.append(example_indent+line.strip()) + else: # not example + parts.append(super()._fill_text(line, width, indent)) + return '\n'.join(parts) + def _format_args(self, action, default_metavar): """Substitute 'METAVAR...' for 'METAVAR [METAVAR ...]'.""" if action.nargs == ONE_OR_MORE: @@ -67,3 +90,12 @@ class NetLoc: return string, self.default_port hostname, port = string.rsplit(':', 1) return hostname, int(port) # ValueError to be handled by argparse + + +def format_examples(examples): + """Format example commands and their description .""" + lines = ['Examples:'] + for example, description in examples: + lines.append(EXAMPLE_PREFIX+example) + lines.append(EXAMPLE_DESCRIPTION_PREFIX+description) + return '\n'.join(lines) diff --git a/src/scadere/check.py b/src/scadere/check.py index 0937e75..380f71c 100644 --- a/src/scadere/check.py +++ b/src/scadere/check.py @@ -27,7 +27,7 @@ from sys import argv, stderr, stdout from unicodedata import category as unicode_category from uuid import uuid4 -from . import __version__, GNUHelpFormatter, NetLoc +from . import __version__, GNUHelpFormatter, NetLoc, format_examples __all__ = ['main'] @@ -94,12 +94,20 @@ def check(netlocs, after, output, fake_ca=None): base64_from_str(ca), file=output) -def main(arguments=argv[1:]): +def main(prog='scadere-check', arguments=argv[1:]): """Run TLS checker.""" - description = ('Check TLS certificate expiration of HOST,' - ' where PORT defaults to 443.') - parser = ArgumentParser(prog='scadere-check', allow_abbrev=False, - description=description, + desc = ('Check TLS certificate expiration of HOST,' + ' where PORT defaults to 443.\n\n' + 'The output is intended to be used by scadere-listen(1).') + examples = [((f'{prog} --output=/var/lib/scadere/certificates' + ' example.com example.net'), + ('Check if TLS certificates used by example.com:443' + ' and example.net:443 are either invalid' + ' or expiring within the next week,' + ' then write the result to /var/lib/scadere/certificates.'))] + + parser = ArgumentParser(prog=prog, allow_abbrev=False, description=desc, + epilog=format_examples(examples), formatter_class=GNUHelpFormatter) parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version__}') @@ -110,6 +118,7 @@ def main(arguments=argv[1:]): parser.add_argument('-o', '--output', metavar='PATH', type=FileType('w'), default=stdout, help='output file (default to stdout)') + args = parser.parse_args(arguments) with args.output: # pragma: no cover after = datetime.now(timezone.utc) + timedelta(days=args.days) diff --git a/src/scadere/listen.py b/src/scadere/listen.py index 649a7cd..2888524 100644 --- a/src/scadere/listen.py +++ b/src/scadere/listen.py @@ -32,7 +32,7 @@ from xml.etree.ElementTree import (Element as xml_element, indent, tostring as string_from_xml) from sys import argv -from . import __version__, GNUHelpFormatter, NetLoc +from . import __version__, GNUHelpFormatter, NetLoc, format_examples from .check import base64_from_str __all__ = ['main'] @@ -312,21 +312,37 @@ def with_trailing_slash(base_url): return base_url if base_url.endswith('/') else f'{base_url}/' -def main(arguments=argv[1:]): +def main(prog='scadere-listen', arguments=argv[1:]): """Launch server.""" - description = ('Serve the TLS certificate expiration feed' - ' from INPUT file for base URL at HOST:PORT,' - ' where HOST defaults to localhost and PORT' - ' is selected randomly if not specified.') - parser = ArgumentParser(prog='scadere-listen', allow_abbrev=False, - description=description, + desc = ('Serve at URL Atom feeds for TLS certificate renewal reminder.' + ' It is possible for clients to filter domains' + ' using one or more "domain" URL queries.\n\n' + 'The certificate information is read from the file at PATH,' + ' which is generated by scadere-check(1).\n\n' + 'The server listens for TCP connections coming to HOST:PORT,' + ' where HOST defaults to localhost' + ' and PORT is selected randomly if not specified.\n\n') + examples = [((f'{prog} /var/lib/scadere/certificates' + ' https://scadere.example/ :4433'), + ('Serve renewal reminder feed using information' + ' from /var/lib/scadere/certificates on localhost:4433,' + ' to be reverse proxied to https://scadere.example/')), + ('https://scadere.example/', + 'Atom feed for all checked TLS certificates'), + ('https://scadere.example/?domain=example.com&domain=net', + ('Atom feed for checked TLS certificates for example.com,' + ' its subdomains, and domains under the TLD NET'))] + + parser = ArgumentParser(prog=prog, allow_abbrev=False, description=desc, + epilog=format_examples(examples), formatter_class=GNUHelpFormatter) parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version__}') - parser.add_argument('certs', metavar='INPUT', type=Path) + parser.add_argument('certs', metavar='PATH', type=Path) parser.add_argument('base_url', metavar='URL', type=with_trailing_slash) parser.add_argument('netloc', metavar='[HOST][:PORT]', nargs='?', type=NetLoc(None), default=('localhost', None)) + args = parser.parse_args(arguments) run(listen(args.certs, args.base_url, *args.netloc)) # pragma: no cover diff --git a/tst/test_help.py b/tst/test_help.py index 3e5ce87..2a6c06a 100644 --- a/tst/test_help.py +++ b/tst/test_help.py @@ -22,7 +22,7 @@ from io import StringIO from hypothesis import example, given from pytest import fixture, mark, raises -from scadere import NetLoc +from scadere import EXAMPLE_PREFIX, EXAMPLE_DESCRIPTION_PREFIX, NetLoc from scadere.check import main as check from scadere.listen import main as listen @@ -31,7 +31,7 @@ from scadere.listen import main as listen def help_string(request): string = StringIO() with suppress(SystemExit), redirect_stdout(string): - request.param(['--help']) + request.param(arguments=['--help']) return string.getvalue() @@ -57,6 +57,26 @@ def test_long_option(help_string, short, long, metavar): assert f'{short} {metavar}, {long}={metavar}' in help_string +@mark.parametrize('help_string', [check, listen], indirect=True) +def test_examples(help_string): + index = help_string.find('\n\nExamples:\n') + assert index >= 0 + lines = help_string[index:].removeprefix('\n\nExamples:\n').splitlines() + + assert EXAMPLE_DESCRIPTION_PREFIX.startswith(EXAMPLE_PREFIX) + assert not lines[0].startswith(EXAMPLE_DESCRIPTION_PREFIX) + assert lines[-1].startswith(EXAMPLE_DESCRIPTION_PREFIX) + + must_be_desc = False + for line in lines: + if must_be_desc: + assert line.startswith(EXAMPLE_DESCRIPTION_PREFIX) + must_be_desc = False + else: + assert line.startswith(EXAMPLE_PREFIX) + must_be_desc = not line.startswith(EXAMPLE_DESCRIPTION_PREFIX) + + @example('a.example:b', None) # string is unlikely to match .*:\D+ @example('a.example:98', None) # string is unlikely to match .*:\d+ @given(...) |