Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions pathspec/patterns/gitignore/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,21 @@

def _strip_trailing_ws(pattern: str) -> str:
"""
Strip trailing whitespace from the pattern while considering that whitespace
can be escaped.
Strip trailing spaces and line endings from the pattern while considering
that they can be escaped. Other whitespace is part of the filename.

*pattern* (:class:`str`) is the pattern.

Returns the modified pattern (:class:`str`).
"""
i = len(pattern) - 1
if i == -1 or not pattern[i].isspace():
# Fast path: pattern does not end with whitespace. Nothing to strip.
if i == -1 or pattern[i] not in ' \r\n':
# Fast path: pattern does not end with a space or line ending.
return pattern

# Scan past whitespace.
# Scan past spaces and line endings.
i -= 1
while i >= 0 and pattern[i].isspace():
while i >= 0 and pattern[i] in ' \r\n':
i -= 1

last_ws = i + 1
Expand Down
15 changes: 13 additions & 2 deletions tests/test_02_gitignore_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_02_strip_trailing_ws_1_even_bs(self):
"""
for n, wc in product(
[0, 2, 4],
[' ', '\n', '\t'],
[' ', '\n', '\r'],
):
with self.subTest(n=n, wc=wc):
bs = BS*n
Expand All @@ -69,10 +69,21 @@ def test_02_strip_trailing_ws_2_odd_bs(self):
"""
for n, wc in product(
[0, 1, 3],
[' ', '\n', '\t'],
[' ', '\n', '\r'],
):
with self.subTest(n=n, wc=wc):
bs = BS*n
ws = wc*n
val = _strip_trailing_ws(f' foo{bs}{ws}')
self.assertEqual(val, f' foo{bs}{ws[:1]}')

def test_02_strip_trailing_ws_3_literal_whitespace(self):
"""
Only spaces and line endings are stripped, not filename characters.
"""
for wc, ending in product(
['\t', '\v', '\f', '\x85', '\xa0', '\u2003'],
['', ' ', '\n', '\r\n'],
):
with self.subTest(wc=wc, ending=ending):
self.assertEqual(_strip_trailing_ws(f'foo{wc}{ending}'), f'foo{wc}')
96 changes: 96 additions & 0 deletions tests/test_07_gitignore_trailing_chars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""
Test that trailing filename characters are not treated as trailing spaces.
"""

import io
import unittest

from pathspec import (
GitIgnoreSpec,
PathSpec)
from pathspec.patterns.gitignore.basic import (
GitIgnoreBasicPattern)
from pathspec.patterns.gitignore.spec import (
GitIgnoreSpecPattern)


class GitIgnoreTrailingCharsTest(unittest.TestCase):
"""
Test literal trailing whitespace in patterns and filename escaping.
"""

def test_01_literal_whitespace(self):
for cls in (GitIgnoreBasicPattern, GitIgnoreSpecPattern):
for suffix in ('\t', '\v', '\f', '\x85', '\xa0', '\u2003'):
name = 'foo' + suffix
for ending in ('', ' ', '\n', '\r\n'):
with self.subTest(cls=cls, suffix=suffix, ending=ending):
pattern = cls(name + ending)
self.assertTrue(pattern.match_file(name))
self.assertIsNone(pattern.match_file('foo'))
escaped = cls(cls.escape(name))
self.assertTrue(escaped.match_file(name))

def test_02_bytes(self):
for cls in (GitIgnoreBasicPattern, GitIgnoreSpecPattern):
for suffix in (b'\t', b'\v', b'\f', b'\x85', b'\xa0', b'\xc2\xa0', b'\xe2\x80\x83'):
name = b'foo' + suffix
for ending in (b'', b' ', b'\n', b'\r\n'):
with self.subTest(cls=cls, suffix=suffix, ending=ending):
pattern = cls(name + ending)
self.assertTrue(pattern.match_file(name))
self.assertIsNone(pattern.match_file(b'foo'))
escaped = cls(cls.escape(name))
self.assertTrue(escaped.match_file(name))

def test_03_stream_lines(self):
for suffix in ('\t', '\xa0', '\u2003'):
name = 'foo' + suffix
for ending in ('\n', '\r\n'):
for make_spec in (
lambda lines: PathSpec.from_lines('gitignore', lines, backend='simple'),
lambda lines: GitIgnoreSpec.from_lines(lines, backend='simple'),
):
with self.subTest(suffix=suffix, ending=ending, make_spec=make_spec):
spec = make_spec(io.StringIO(name + ending))
self.assertTrue(spec.match_file(name))
self.assertFalse(spec.match_file('foo'))

def test_04_spaces_and_backslashes(self):
for cls in (GitIgnoreBasicPattern, GitIgnoreSpecPattern):
for raw, name in (
('foo ', 'foo'),
('foo\\ ', 'foo '),
('foo\\\\ ', 'foo\\'),
('foo\\\\\\ ', 'foo\\ '),
('foo\\\t ', 'foo\t'),
):
for ending in ('', '\n', '\r\n'):
with self.subTest(cls=cls, raw=raw, ending=ending):
self.assertTrue(cls(raw + ending).match_file(name))

def test_05_negation_and_directories(self):
for suffix in ('\t', '\xa0', '\u2003'):
name = 'foo' + suffix
with self.subTest(suffix=suffix):
spec = GitIgnoreSpec.from_lines(['*', '!' + name], backend='simple')
self.assertFalse(spec.match_file(name))
self.assertTrue(spec.match_file('foo'))
spec = GitIgnoreSpec.from_lines([name + '/'], backend='simple')
self.assertTrue(spec.match_file(name + '/'))
self.assertTrue(spec.match_file(name + '/child'))
self.assertFalse(spec.match_file(name))
self.assertFalse(spec.match_file('foo/child'))

def test_06_character_after_slash(self):
for suffix in ('\t', '\xa0', '\u2003'):
name = 'foo/' + suffix
for make_spec in (
lambda lines: PathSpec.from_lines('gitignore', lines, backend='simple'),
lambda lines: GitIgnoreSpec.from_lines(lines, backend='simple'),
):
with self.subTest(suffix=suffix, make_spec=make_spec):
spec = make_spec([name])
self.assertTrue(spec.match_file(name))
self.assertFalse(spec.match_file('foo/'))
self.assertFalse(spec.match_file('foo/other'))