1
0
Fork 0
mirror of https://github.com/borgbackup/borg.git synced 2025-02-20 21:27:32 +00:00

nanorst for --help

This commit is contained in:
Marian Beermann 2017-06-07 13:50:20 +02:00
parent 2118fb45a2
commit 1cf031045c
4 changed files with 250 additions and 3 deletions

View file

@ -64,6 +64,7 @@
from .helpers import replace_placeholders from .helpers import replace_placeholders
from .helpers import ChunkIteratorFileWrapper from .helpers import ChunkIteratorFileWrapper
from .helpers import popen_with_error_handling from .helpers import popen_with_error_handling
from .nanorst import RstToTextLazy, ansi_escapes
from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern
from .patterns import PatternMatcher from .patterns import PatternMatcher
from .item import Item from .item import Item
@ -2256,6 +2257,18 @@ def resolve(self, args: argparse.Namespace): # Namespace has "in" but otherwise
setattr(args, dest, option_value) setattr(args, dest, option_value)
def build_parser(self): def build_parser(self):
if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() and (sys.platform != 'win32' or 'ANSICON' in os.environ):
rst_state_hook = ansi_escapes
else:
rst_state_hook = None
# You can use :ref:`xyz` in the following usage pages. However, for plain-text view,
# e.g. through "borg ... --help", define a substitution for the reference here.
# It will replace the entire :ref:`foo` verbatim.
rst_plain_text_references = {
'a_status_oddity': '"I am seeing A (added) status for a unchanged file!?"',
}
def process_epilog(epilog): def process_epilog(epilog):
epilog = textwrap.dedent(epilog).splitlines() epilog = textwrap.dedent(epilog).splitlines()
try: try:
@ -2264,7 +2277,10 @@ def process_epilog(epilog):
mode = 'command-line' mode = 'command-line'
if mode in ('command-line', 'build_usage'): if mode in ('command-line', 'build_usage'):
epilog = [line for line in epilog if not line.startswith('.. man')] epilog = [line for line in epilog if not line.startswith('.. man')]
return '\n'.join(epilog) epilog = '\n'.join(epilog)
if mode == 'command-line':
epilog = RstToTextLazy(epilog, rst_state_hook, rst_plain_text_references)
return epilog
def define_common_options(add_common_option): def define_common_options(add_common_option):
add_common_option('-h', '--help', action='help', help='show this help message and exit') add_common_option('-h', '--help', action='help', help='show this help message and exit')
@ -3451,7 +3467,7 @@ def define_common_options(add_common_option):
used to have upgraded Borg 0.xx or Attic archives deduplicate with used to have upgraded Borg 0.xx or Attic archives deduplicate with
Borg 1.x archives. Borg 1.x archives.
USE WITH CAUTION. **USE WITH CAUTION.**
Depending on the PATHs and patterns given, recreate can be used to permanently Depending on the PATHs and patterns given, recreate can be used to permanently
delete files from archives. delete files from archives.
When in doubt, use "--dry-run --verbose --list" to see how patterns/PATHS are When in doubt, use "--dry-run --verbose --list" to see how patterns/PATHS are
@ -3761,7 +3777,7 @@ def define_common_options(add_common_option):
It creates input data below the given PATH and backups this data into the given REPO. It creates input data below the given PATH and backups this data into the given REPO.
The REPO must already exist (it could be a fresh empty repo or an existing repo, the The REPO must already exist (it could be a fresh empty repo or an existing repo, the
command will create / read / update / delete some archives named borg-test-data* there. command will create / read / update / delete some archives named borg-test-data\* there.
Make sure you have free space there, you'll need about 1GB each (+ overhead). Make sure you have free space there, you'll need about 1GB each (+ overhead).

162
src/borg/nanorst.py Normal file
View file

@ -0,0 +1,162 @@
import io
class TextPecker:
def __init__(self, s):
self.str = s
self.i = 0
def read(self, n):
self.i += n
return self.str[self.i - n:self.i]
def peek(self, n):
if n >= 0:
return self.str[self.i:self.i + n]
else:
return self.str[self.i + n - 1:self.i - 1]
def peekline(self):
out = ''
i = self.i
while i < len(self.str) and self.str[i] != '\n':
out += self.str[i]
i += 1
return out
def readline(self):
out = self.peekline()
self.i += len(out)
return out
def rst_to_text(text, state_hook=None, references=None):
"""
Convert rST to a more human text form.
This is a very loose conversion. No advanced rST features are supported.
The generated output directly depends on the input (e.g. indentation of
admonitions).
"""
state_hook = state_hook or (lambda old_state, new_state, out: None)
references = references or {}
state = 'text'
text = TextPecker(text)
out = io.StringIO()
inline_single = ('*', '`')
while True:
char = text.read(1)
if not char:
break
next = text.peek(1) # type: str
if state == 'text':
if text.peek(-1) != '\\':
if char in inline_single and next not in inline_single:
state_hook(state, char, out)
state = char
continue
if char == next == '*':
state_hook(state, '**', out)
state = '**'
text.read(1)
continue
if char == next == '`':
state_hook(state, '``', out)
state = '``'
text.read(1)
continue
if text.peek(-1).isspace() and char == ':' and text.peek(5) == 'ref:`':
# translate reference
text.read(5)
ref = ''
while True:
char = text.peek(1)
if char == '`':
text.read(1)
break
if char == '\n':
text.read(1)
continue # merge line breaks in :ref:`...\n...`
ref += text.read(1)
try:
out.write(references[ref])
except KeyError:
raise ValueError("Undefined reference in Archiver help: %r — please add reference substitution"
"to 'rst_plain_text_references'" % ref)
continue
if text.peek(-2) in ('\n\n', '') and char == next == '.':
text.read(2)
try:
directive, arguments = text.peekline().split('::', maxsplit=1)
except ValueError:
directive = None
text.readline()
text.read(1)
if not directive:
continue
out.write(directive.title())
out.write(':\n')
if arguments:
out.write(arguments)
out.write('\n')
continue
if state in inline_single and char == state:
state_hook(state, 'text', out)
state = 'text'
continue
if state == '``' and char == next == '`':
state_hook(state, 'text', out)
state = 'text'
text.read(1)
continue
if state == '**' and char == next == '*':
state_hook(state, 'text', out)
state = 'text'
text.read(1)
continue
out.write(char)
assert state == 'text', 'Invalid final state %r (This usually indicates unmatched */**)' % state
return out.getvalue()
def ansi_escapes(old_state, new_state, out):
if old_state == 'text' and new_state in ('*', '`', '``'):
out.write('\033[4m')
if old_state == 'text' and new_state == '**':
out.write('\033[1m')
if old_state in ('*', '`', '``', '**') and new_state == 'text':
out.write('\033[0m')
class RstToTextLazy:
def __init__(self, str, state_hook=None, references=None):
self.str = str
self.state_hook = state_hook
self.references = references
self._rst = None
@property
def rst(self):
if self._rst is None:
self._rst = rst_to_text(self.str, self.state_hook, self.references)
return self._rst
def __getattr__(self, item):
return getattr(self.rst, item)
def __str__(self):
return self.rst
def __add__(self, other):
return self.rst + other
def __iter__(self):
return iter(self.rst)
def __contains__(self, item):
return item in self.rst

View file

@ -30,6 +30,7 @@
except ImportError: except ImportError:
pass pass
import borg
from .. import xattr, helpers, platform from .. import xattr, helpers, platform
from ..archive import Archive, ChunkBuffer, flags_noatime, flags_normal from ..archive import Archive, ChunkBuffer, flags_noatime, flags_normal
from ..archiver import Archiver, parse_storage_quota from ..archiver import Archiver, parse_storage_quota
@ -44,6 +45,7 @@
from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR
from ..helpers import bin_to_hex from ..helpers import bin_to_hex
from ..helpers import MAX_S from ..helpers import MAX_S
from ..nanorst import RstToTextLazy
from ..patterns import IECommand, PatternMatcher, parse_pattern from ..patterns import IECommand, PatternMatcher, parse_pattern
from ..item import Item from ..item import Item
from ..logger import setup_logging from ..logger import setup_logging
@ -3335,3 +3337,33 @@ def test_parse_storage_quota():
assert parse_storage_quota('50M') == 50 * 1000**2 assert parse_storage_quota('50M') == 50 * 1000**2
with pytest.raises(argparse.ArgumentTypeError): with pytest.raises(argparse.ArgumentTypeError):
parse_storage_quota('5M') parse_storage_quota('5M')
def get_all_parsers():
"""
Return dict mapping command to parser.
"""
parser = Archiver(prog='borg').build_parser()
parsers = {}
def discover_level(prefix, parser, Archiver):
choices = {}
for action in parser._actions:
if action.choices is not None and 'SubParsersAction' in str(action.__class__):
for cmd, parser in action.choices.items():
choices[prefix + cmd] = parser
if prefix and not choices:
return
for command, parser in sorted(choices.items()):
discover_level(command + " ", parser, Archiver)
parsers[command] = parser
discover_level("", parser, Archiver)
return parsers
@pytest.mark.parametrize('command, parser', list(get_all_parsers().items()))
def test_help_formatting(command, parser):
if isinstance(parser.epilog, RstToTextLazy):
assert parser.epilog.rst

View file

@ -0,0 +1,37 @@
import pytest
from ..nanorst import rst_to_text
def test_inline():
assert rst_to_text('*foo* and ``bar``.') == 'foo and bar.'
def test_inline_spread():
assert rst_to_text('*foo and bar, thusly\nfoobar*.') == 'foo and bar, thusly\nfoobar.'
def test_comment_inline():
assert rst_to_text('Foo and Bar\n.. foo\nbar') == 'Foo and Bar\n.. foo\nbar'
def test_comment():
assert rst_to_text('Foo and Bar\n\n.. foo\nbar') == 'Foo and Bar\n\nbar'
def test_directive_note():
assert rst_to_text('.. note::\n Note this and that') == 'Note:\n Note this and that'
def test_ref():
references = {
'foo': 'baz'
}
assert rst_to_text('See :ref:`fo\no`.', references=references) == 'See baz.'
def test_undefined_ref():
with pytest.raises(ValueError) as exc_info:
rst_to_text('See :ref:`foo`.')
assert 'Undefined reference' in str(exc_info.value)