mirror of
https://github.com/borgbackup/borg.git
synced 2025-02-19 04:41:50 +00:00
nanorst for --help
This commit is contained in:
parent
2118fb45a2
commit
1cf031045c
4 changed files with 250 additions and 3 deletions
|
@ -64,6 +64,7 @@
|
|||
from .helpers import replace_placeholders
|
||||
from .helpers import ChunkIteratorFileWrapper
|
||||
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 PatternMatcher
|
||||
from .item import Item
|
||||
|
@ -2256,6 +2257,18 @@ def resolve(self, args: argparse.Namespace): # Namespace has "in" but otherwise
|
|||
setattr(args, dest, option_value)
|
||||
|
||||
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):
|
||||
epilog = textwrap.dedent(epilog).splitlines()
|
||||
try:
|
||||
|
@ -2264,7 +2277,10 @@ def process_epilog(epilog):
|
|||
mode = 'command-line'
|
||||
if mode in ('command-line', 'build_usage'):
|
||||
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):
|
||||
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
|
||||
Borg 1.x archives.
|
||||
|
||||
USE WITH CAUTION.
|
||||
**USE WITH CAUTION.**
|
||||
Depending on the PATHs and patterns given, recreate can be used to permanently
|
||||
delete files from archives.
|
||||
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.
|
||||
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).
|
||||
|
||||
|
|
162
src/borg/nanorst.py
Normal file
162
src/borg/nanorst.py
Normal 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
|
|
@ -30,6 +30,7 @@
|
|||
except ImportError:
|
||||
pass
|
||||
|
||||
import borg
|
||||
from .. import xattr, helpers, platform
|
||||
from ..archive import Archive, ChunkBuffer, flags_noatime, flags_normal
|
||||
from ..archiver import Archiver, parse_storage_quota
|
||||
|
@ -44,6 +45,7 @@
|
|||
from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR
|
||||
from ..helpers import bin_to_hex
|
||||
from ..helpers import MAX_S
|
||||
from ..nanorst import RstToTextLazy
|
||||
from ..patterns import IECommand, PatternMatcher, parse_pattern
|
||||
from ..item import Item
|
||||
from ..logger import setup_logging
|
||||
|
@ -3335,3 +3337,33 @@ def test_parse_storage_quota():
|
|||
assert parse_storage_quota('50M') == 50 * 1000**2
|
||||
with pytest.raises(argparse.ArgumentTypeError):
|
||||
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
|
||||
|
|
37
src/borg/testsuite/nanorst.py
Normal file
37
src/borg/testsuite/nanorst.py
Normal 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)
|
Loading…
Reference in a new issue