nanorst for "borg help TOPIC"

This commit is contained in:
Marian Beermann 2017-06-09 19:22:18 +02:00
parent 9bc00f01aa
commit 827c478500
7 changed files with 88 additions and 40 deletions

View File

@ -24,7 +24,7 @@ This command benchmarks borg CRUD (create, read, update, delete) operations.
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).

View File

@ -8,8 +8,10 @@ borg help patterns
File patterns support these styles: fnmatch, shell, regular expressions,
path prefixes and path full-matches. By default, fnmatch is used for
`--exclude` patterns and shell-style is used for `--pattern`. If followed
by a colon (':') the first two characters of a pattern are used as a
`--exclude` patterns and shell-style is used for the experimental `--pattern`
option.
If followed by a colon (':') the first two characters of a pattern are used as a
style selector. Explicit style selection is necessary when a
non-default style is desired or when the desired pattern starts with
two alphanumeric characters followed by a colon (i.e. `aa:something/*`).
@ -17,7 +19,7 @@ two alphanumeric characters followed by a colon (i.e. `aa:something/*`).
`Fnmatch <https://docs.python.org/3/library/fnmatch.html>`_, selector `fm:`
This is the default style for --exclude and --exclude-from.
These patterns use a variant of shell pattern syntax, with '*' matching
These patterns use a variant of shell pattern syntax, with '\*' matching
any number of characters, '?' matching any single character, '[...]'
matching any single character specified, including ranges, and '[!...]'
matching any character not specified. For the purpose of these patterns,

View File

@ -82,7 +82,7 @@ There is no risk of data loss by this.
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

View File

@ -64,7 +64,7 @@ from .helpers import basic_json_data, json_print
from .helpers import replace_placeholders
from .helpers import ChunkIteratorFileWrapper
from .helpers import popen_with_error_handling
from .nanorst import RstToTextLazy, ansi_escapes
from .nanorst import rst_to_terminal
from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern
from .patterns import PatternMatcher
from .item import Item
@ -1837,8 +1837,10 @@ class Archiver:
helptext['patterns'] = textwrap.dedent('''
File patterns support these styles: fnmatch, shell, regular expressions,
path prefixes and path full-matches. By default, fnmatch is used for
`--exclude` patterns and shell-style is used for `--pattern`. If followed
by a colon (':') the first two characters of a pattern are used as a
`--exclude` patterns and shell-style is used for the experimental `--pattern`
option.
If followed by a colon (':') the first two characters of a pattern are used as a
style selector. Explicit style selection is necessary when a
non-default style is desired or when the desired pattern starts with
two alphanumeric characters followed by a colon (i.e. `aa:something/*`).
@ -1846,7 +1848,7 @@ class Archiver:
`Fnmatch <https://docs.python.org/3/library/fnmatch.html>`_, selector `fm:`
This is the default style for --exclude and --exclude-from.
These patterns use a variant of shell pattern syntax, with '*' matching
These patterns use a variant of shell pattern syntax, with '\*' matching
any number of characters, '?' matching any single character, '[...]'
matching any single character specified, including ranges, and '[!...]'
matching any character not specified. For the purpose of these patterns,
@ -1857,7 +1859,7 @@ class Archiver:
must match from the start to just before a path separator. Except
for the root path, paths will never end in the path separator when
matching is attempted. Thus, if a given pattern ends in a path
separator, a '*' is appended before matching is attempted.
separator, a '\*' is appended before matching is attempted.
Shell-style patterns, selector `sh:`
@ -2099,7 +2101,7 @@ class Archiver:
if not args.topic:
parser.print_help()
elif args.topic in self.helptext:
print(self.helptext[args.topic])
print(rst_to_terminal(self.helptext[args.topic]))
elif args.topic in commands:
if args.epilog_only:
print(commands[args.topic].epilog)
@ -2257,11 +2259,6 @@ class Archiver:
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.
@ -2279,7 +2276,7 @@ class Archiver:
epilog = [line for line in epilog if not line.startswith('.. man')]
epilog = '\n'.join(epilog)
if mode == 'command-line':
epilog = RstToTextLazy(epilog, rst_state_hook, rst_plain_text_references)
epilog = rst_to_terminal(epilog, rst_plain_text_references)
return epilog
def define_common_options(add_common_option):

View File

@ -1,5 +1,6 @@
import io
import os
import sys
class TextPecker:
@ -31,6 +32,21 @@ class TextPecker:
return out
def process_directive(directive, arguments, out, state_hook):
if directive == 'container' and arguments == 'experimental':
state_hook('text', '**', out)
out.write('++ Experimental ++')
state_hook('**', 'text', out)
else:
state_hook('text', '**', out)
out.write(directive.title())
out.write(':\n')
state_hook('**', 'text', out)
if arguments:
out.write(arguments)
out.write('\n')
def rst_to_text(text, state_hook=None, references=None):
"""
Convert rST to a more human text form.
@ -54,8 +70,10 @@ def rst_to_text(text, state_hook=None, references=None):
next = text.peek(1) # type: str
if state == 'text':
if char == '\\' and text.peek(1) in inline_single:
continue
if text.peek(-1) != '\\':
if char in inline_single and next not in inline_single:
if char in inline_single and next != char:
state_hook(state, char, out)
state = char
continue
@ -88,21 +106,19 @@ def rst_to_text(text, state_hook=None, references=None):
raise ValueError("Undefined reference in Archiver help: %r — please add reference substitution"
"to 'rst_plain_text_references'" % ref)
continue
if char == ':' and text.peek(2) == ':\n': # End of line code block
text.read(2)
state_hook(state, 'code-block', out)
state = 'code-block'
out.write(':\n')
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()
directive, is_directive, arguments = text.readline().partition('::')
text.read(1)
if not directive:
if not is_directive:
continue
out.write(directive.title())
out.write(':\n')
if arguments:
out.write(arguments)
out.write('\n')
process_directive(directive, arguments.strip(), out, state_hook)
continue
if state in inline_single and char == state:
state_hook(state, 'text', out)
@ -118,21 +134,22 @@ def rst_to_text(text, state_hook=None, references=None):
state = 'text'
text.read(1)
continue
if state == 'code-block' and char == next == '\n' and text.peek(5)[1:] != ' ':
# Foo::
#
# *stuff* *code* *ignore .. all markup*
#
# More arcane stuff
#
# Regular text...
state_hook(state, 'text', out)
state = 'text'
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
@ -160,3 +177,26 @@ class RstToTextLazy:
def __contains__(self, item):
return item in self.rst
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')
def rst_to_terminal(rst, references=None, destination=sys.stdout):
"""
Convert *rst* to a lazy string.
If *destination* is a file-like object connected to a terminal,
enrich text with suitable ANSI escapes. Otherwise return plain text.
"""
if hasattr(destination, 'isatty') and destination.isatty() and (sys.platform != 'win32' or 'ANSICON' in os.environ):
rst_state_hook = ansi_escapes
else:
rst_state_hook = None
return RstToTextLazy(rst, rst_state_hook, references)

View File

@ -45,7 +45,7 @@ from ..helpers import Manifest
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 ..nanorst import RstToTextLazy, rst_to_terminal
from ..patterns import IECommand, PatternMatcher, parse_pattern
from ..item import Item
from ..logger import setup_logging
@ -3366,3 +3366,8 @@ def get_all_parsers():
def test_help_formatting(command, parser):
if isinstance(parser.epilog, RstToTextLazy):
assert parser.epilog.rst
@pytest.mark.parametrize('topic, helptext', list(Archiver.helptext.items()))
def test_help_formatting_helptexts(topic, helptext):
assert str(rst_to_terminal(helptext))

View File

@ -16,6 +16,10 @@ def test_comment_inline():
assert rst_to_text('Foo and Bar\n.. foo\nbar') == 'Foo and Bar\n.. foo\nbar'
def test_inline_escape():
assert rst_to_text('Such as "\\*" characters.') == 'Such as "*" characters.'
def test_comment():
assert rst_to_text('Foo and Bar\n\n.. foo\nbar') == 'Foo and Bar\n\nbar'