Fix patterns parsing #976

Merged
witten merged 2 commits from pasha132/borgmatic:fix-exclude-parser into main 2025-01-23 20:06:12 +00:00
2 changed files with 28 additions and 23 deletions

View file

@ -15,7 +15,7 @@ import borgmatic.hooks.dispatch
logger = logging.getLogger(__name__)
def parse_pattern(pattern_line):
def parse_pattern(pattern_line, default_style=borgmatic.borg.pattern.Pattern_style.NONE):
'''
Given a Borg pattern as a string, parse it into a borgmatic.borg.pattern.Pattern instance and
return it.
@ -26,9 +26,10 @@ def parse_pattern(pattern_line):
raise ValueError(f'Invalid pattern: {pattern_line}')
try:
(pattern_style, path) = remainder.split(':', maxsplit=1)
(parsed_pattern_style, path) = remainder.split(':', maxsplit=1)
pattern_style = borgmatic.borg.pattern.Pattern_style(parsed_pattern_style)
except ValueError:
pattern_style = ''
pattern_style = default_style
path = remainder
return borgmatic.borg.pattern.Pattern(
@ -54,32 +55,26 @@ def collect_patterns(config):
for source_directory in config.get('source_directories', ())
)
+ tuple(
parse_pattern(pattern_line.strip())
parse_pattern(pattern_line.strip(), borgmatic.borg.pattern.Pattern_style.SHELL)
for pattern_line in config.get('patterns', ())
if not pattern_line.lstrip().startswith('#')
if pattern_line.strip()
)
+ tuple(
borgmatic.borg.pattern.Pattern(
exclude_line.strip(),
borgmatic.borg.pattern.Pattern_type.EXCLUDE,
borgmatic.borg.pattern.Pattern_style.FNMATCH,
)
parse_pattern(f'{borgmatic.borg.pattern.Pattern_type.EXCLUDE.value} {
exclude_line.strip()}', borgmatic.borg.pattern.Pattern_style.FNMATCH)
for exclude_line in config.get('exclude_patterns', ())
)
+ tuple(
parse_pattern(pattern_line.strip())
parse_pattern(pattern_line.strip(), borgmatic.borg.pattern.Pattern_style.SHELL)
for filename in config.get('patterns_from', ())
for pattern_line in open(filename).readlines()
if not pattern_line.lstrip().startswith('#')
if pattern_line.strip()

Since you now have the handy default_style parameter, what do you think of adding default_sytle=borgmatic.borg.pattern.Pattern_style.SHELL for the patterns and patterns_from options here? Based on these docs, that'd be more correct than the existing behavior prior to your changes.

Since you now have the handy `default_style` parameter, what do you think of adding `default_sytle=borgmatic.borg.pattern.Pattern_style.SHELL` for the `patterns` and `patterns_from` options here? Based on [these docs](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns), that'd be more correct than the existing behavior prior to your changes.

Turns out this was a terrible idea.. My bad for suggesting it. The problem was that tacking on "sh:" unconditionally breaks root patterns in particular (e.g. R /foo/bar), because they don't have pattern style prefixes.

borgmatic 1.9.8 fixes that.

Turns out this was [a terrible idea](https://projects.torsion.org/borgmatic-collective/borgmatic/issues/979).. My bad for suggesting it. The problem was that tacking on "sh:" unconditionally breaks root patterns in particular (e.g. `R /foo/bar`), because they don't have pattern style prefixes. borgmatic 1.9.8 fixes that.
)
+ tuple(
borgmatic.borg.pattern.Pattern(
exclude_line.strip(),
borgmatic.borg.pattern.Pattern_type.EXCLUDE,
borgmatic.borg.pattern.Pattern_style.FNMATCH,
)
parse_pattern(f'{borgmatic.borg.pattern.Pattern_type.EXCLUDE.value} {
exclude_line.strip()}', borgmatic.borg.pattern.Pattern_style.FNMATCH)
for filename in config.get('exclude_from', ())
for exclude_line in open(filename).readlines()
if not exclude_line.lstrip().startswith('#')

View file

@ -34,11 +34,13 @@ def test_collect_patterns_converts_source_directories():
def test_collect_patterns_parses_config_patterns():
flexmock(module).should_receive('parse_pattern').with_args('R /foo').and_return(Pattern('/foo'))
flexmock(module).should_receive('parse_pattern').with_args(
'R /foo', Pattern_style.SHELL).and_return(Pattern('/foo'))
flexmock(module).should_receive('parse_pattern').with_args('# comment').never()
flexmock(module).should_receive('parse_pattern').with_args('').never()
flexmock(module).should_receive('parse_pattern').with_args(' ').never()
flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar'))
flexmock(module).should_receive('parse_pattern').with_args(
'R /bar', Pattern_style.SHELL).and_return(Pattern('/bar'))
assert module.collect_patterns({'patterns': ['R /foo', '# comment', '', ' ', 'R /bar']}) == (
Pattern('/foo'),
@ -47,9 +49,10 @@ def test_collect_patterns_parses_config_patterns():
def test_collect_patterns_converts_exclude_patterns():
assert module.collect_patterns({'exclude_patterns': ['/foo', '/bar']}) == (
assert module.collect_patterns({'exclude_patterns': ['/foo', '/bar', 'sh:**/baz']}) == (
Pattern('/foo', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
Pattern('/bar', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
Pattern('**/baz', Pattern_type.EXCLUDE, Pattern_style.SHELL),
)
@ -59,12 +62,15 @@ def test_collect_patterns_reads_config_patterns_from_file():
builtins.should_receive('open').with_args('file2.txt').and_return(
io.StringIO('R /bar\n# comment\n\n \nR /baz')
)
flexmock(module).should_receive('parse_pattern').with_args('R /foo').and_return(Pattern('/foo'))
flexmock(module).should_receive('parse_pattern').with_args(
'R /foo', Pattern_style.SHELL).and_return(Pattern('/foo'))
flexmock(module).should_receive('parse_pattern').with_args('# comment').never()
flexmock(module).should_receive('parse_pattern').with_args('').never()
flexmock(module).should_receive('parse_pattern').with_args(' ').never()
flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar'))
flexmock(module).should_receive('parse_pattern').with_args('R /baz').and_return(Pattern('/baz'))
flexmock(module).should_receive('parse_pattern').with_args(
'R /bar', Pattern_style.SHELL).and_return(Pattern('/bar'))
flexmock(module).should_receive('parse_pattern').with_args(
'R /baz', Pattern_style.SHELL).and_return(Pattern('/baz'))
assert module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']}) == (
Pattern('/foo'),
@ -88,11 +94,15 @@ def test_collect_patterns_reads_config_exclude_from_file():
builtins.should_receive('open').with_args('file2.txt').and_return(
io.StringIO('/bar\n# comment\n\n \n/baz')
)
flexmock(module).should_receive('parse_pattern').with_args('/bar').and_return(Pattern('/bar'))

Sorry, my mistake: never used flexmock before. Just saw that without this code test didn't fail.

Sorry, my mistake: never used flexmock before. Just saw that without this code test didn't fail.

That's understandable!

That's understandable!
flexmock(module).should_receive('parse_pattern').with_args(
'- /foo', default_style=Pattern_style.FNMATCH).and_return(Pattern('/foo', Pattern_type.EXCLUDE, Pattern_style.FNMATCH))
flexmock(module).should_receive('parse_pattern').with_args(
'- /bar', default_style=Pattern_style.FNMATCH).and_return(Pattern('/bar', Pattern_type.EXCLUDE, Pattern_style.FNMATCH))
flexmock(module).should_receive('parse_pattern').with_args('# comment').never()
flexmock(module).should_receive('parse_pattern').with_args('').never()
flexmock(module).should_receive('parse_pattern').with_args(' ').never()
flexmock(module).should_receive('parse_pattern').with_args('/baz').and_return(Pattern('/baz'))
flexmock(module).should_receive('parse_pattern').with_args(
'- /baz', default_style=Pattern_style.FNMATCH).and_return(Pattern('/baz', Pattern_type.EXCLUDE, Pattern_style.FNMATCH))
assert module.collect_patterns({'exclude_from': ['file1.txt', 'file2.txt']}) == (
Pattern('/foo', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),