~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/globbing.py

  • Committer: Martin Packman
  • Date: 2011-11-17 13:45:49 UTC
  • mto: This revision was merged to the branch mainline in revision 6271.
  • Revision ID: martin.packman@canonical.com-20111117134549-080e1fhtrzoicexg
Only assert FileExists path in test_transform directory clash tests to avoid stringification fallout

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2011 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Tools for converting globs to regular expressions.
 
18
 
 
19
This module provides functions for converting shell-like globs to regular
 
20
expressions.
 
21
"""
 
22
 
 
23
import re
 
24
 
 
25
from bzrlib import (
 
26
    errors,
 
27
    lazy_regex,
 
28
    )
 
29
from bzrlib.trace import (
 
30
    mutter,
 
31
    warning,
 
32
    )
 
33
 
 
34
 
 
35
class Replacer(object):
 
36
    """Do a multiple-pattern substitution.
 
37
 
 
38
    The patterns and substitutions are combined into one, so the result of
 
39
    one replacement is never substituted again. Add the patterns and
 
40
    replacements via the add method and then call the object. The patterns
 
41
    must not contain capturing groups.
 
42
    """
 
43
 
 
44
    _expand = lazy_regex.lazy_compile(ur'\\&')
 
45
 
 
46
    def __init__(self, source=None):
 
47
        self._pat = None
 
48
        if source:
 
49
            self._pats = list(source._pats)
 
50
            self._funs = list(source._funs)
 
51
        else:
 
52
            self._pats = []
 
53
            self._funs = []
 
54
 
 
55
    def add(self, pat, fun):
 
56
        r"""Add a pattern and replacement.
 
57
 
 
58
        The pattern must not contain capturing groups.
 
59
        The replacement might be either a string template in which \& will be
 
60
        replaced with the match, or a function that will get the matching text
 
61
        as argument. It does not get match object, because capturing is
 
62
        forbidden anyway.
 
63
        """
 
64
        self._pat = None
 
65
        self._pats.append(pat)
 
66
        self._funs.append(fun)
 
67
 
 
68
    def add_replacer(self, replacer):
 
69
        r"""Add all patterns from another replacer.
 
70
 
 
71
        All patterns and replacements from replacer are appended to the ones
 
72
        already defined.
 
73
        """
 
74
        self._pat = None
 
75
        self._pats.extend(replacer._pats)
 
76
        self._funs.extend(replacer._funs)
 
77
 
 
78
    def __call__(self, text):
 
79
        if not self._pat:
 
80
            self._pat = lazy_regex.lazy_compile(
 
81
                    u'|'.join([u'(%s)' % p for p in self._pats]),
 
82
                    re.UNICODE)
 
83
        return self._pat.sub(self._do_sub, text)
 
84
 
 
85
    def _do_sub(self, m):
 
86
        fun = self._funs[m.lastindex - 1]
 
87
        if hasattr(fun, '__call__'):
 
88
            return fun(m.group(0))
 
89
        else:
 
90
            return self._expand.sub(m.group(0), fun)
 
91
 
 
92
 
 
93
_sub_named = Replacer()
 
94
_sub_named.add(ur'\[:digit:\]', ur'\d')
 
95
_sub_named.add(ur'\[:space:\]', ur'\s')
 
96
_sub_named.add(ur'\[:alnum:\]', ur'\w')
 
97
_sub_named.add(ur'\[:ascii:\]', ur'\0-\x7f')
 
98
_sub_named.add(ur'\[:blank:\]', ur' \t')
 
99
_sub_named.add(ur'\[:cntrl:\]', ur'\0-\x1f\x7f-\x9f')
 
100
 
 
101
 
 
102
def _sub_group(m):
 
103
    if m[1] in (u'!', u'^'):
 
104
        return u'[^' + _sub_named(m[2:-1]) + u']'
 
105
    return u'[' + _sub_named(m[1:-1]) + u']'
 
106
 
 
107
 
 
108
def _invalid_regex(repl):
 
109
    def _(m):
 
110
        warning(u"'%s' not allowed within a regular expression. "
 
111
                "Replacing with '%s'" % (m, repl))
 
112
        return repl
 
113
    return _
 
114
 
 
115
 
 
116
def _trailing_backslashes_regex(m):
 
117
    """Check trailing backslashes.
 
118
 
 
119
    Does a head count on trailing backslashes to ensure there isn't an odd
 
120
    one on the end that would escape the brackets we wrap the RE in.
 
121
    """
 
122
    if (len(m) % 2) != 0:
 
123
        warning(u"Regular expressions cannot end with an odd number of '\\'. "
 
124
                "Dropping the final '\\'.")
 
125
        return m[:-1]
 
126
    return m
 
127
 
 
128
 
 
129
_sub_re = Replacer()
 
130
_sub_re.add(u'^RE:', u'')
 
131
_sub_re.add(u'\((?!\?)', u'(?:')
 
132
_sub_re.add(u'\(\?P<.*>', _invalid_regex(u'(?:'))
 
133
_sub_re.add(u'\(\?P=[^)]*\)', _invalid_regex(u''))
 
134
_sub_re.add(ur'\\+$', _trailing_backslashes_regex)
 
135
 
 
136
 
 
137
_sub_fullpath = Replacer()
 
138
_sub_fullpath.add(ur'^RE:.*', _sub_re) # RE:<anything> is a regex
 
139
_sub_fullpath.add(ur'\[\^?\]?(?:[^][]|\[:[^]]+:\])+\]', _sub_group) # char group
 
140
_sub_fullpath.add(ur'(?:(?<=/)|^)(?:\.?/)+', u'') # canonicalize path
 
141
_sub_fullpath.add(ur'\\.', ur'\&') # keep anything backslashed
 
142
_sub_fullpath.add(ur'[(){}|^$+.]', ur'\\&') # escape specials
 
143
_sub_fullpath.add(ur'(?:(?<=/)|^)\*\*+/', ur'(?:.*/)?') # **/ after ^ or /
 
144
_sub_fullpath.add(ur'\*+', ur'[^/]*') # * elsewhere
 
145
_sub_fullpath.add(ur'\?', ur'[^/]') # ? everywhere
 
146
 
 
147
 
 
148
_sub_basename = Replacer()
 
149
_sub_basename.add(ur'\[\^?\]?(?:[^][]|\[:[^]]+:\])+\]', _sub_group) # char group
 
150
_sub_basename.add(ur'\\.', ur'\&') # keep anything backslashed
 
151
_sub_basename.add(ur'[(){}|^$+.]', ur'\\&') # escape specials
 
152
_sub_basename.add(ur'\*+', ur'.*') # * everywhere
 
153
_sub_basename.add(ur'\?', ur'.') # ? everywhere
 
154
 
 
155
 
 
156
def _sub_extension(pattern):
 
157
    return _sub_basename(pattern[2:])
 
158
 
 
159
 
 
160
class Globster(object):
 
161
    """A simple wrapper for a set of glob patterns.
 
162
 
 
163
    Provides the capability to search the patterns to find a match for
 
164
    a given filename (including the full path).
 
165
 
 
166
    Patterns are translated to regular expressions to expidite matching.
 
167
 
 
168
    The regular expressions for multiple patterns are aggregated into
 
169
    a super-regex containing groups of up to 99 patterns.
 
170
    The 99 limitation is due to the grouping limit of the Python re module.
 
171
    The resulting super-regex and associated patterns are stored as a list of
 
172
    (regex,[patterns]) in _regex_patterns.
 
173
 
 
174
    For performance reasons the patterns are categorised as extension patterns
 
175
    (those that match against a file extension), basename patterns
 
176
    (those that match against the basename of the filename),
 
177
    and fullpath patterns (those that match against the full path).
 
178
    The translations used for extensions and basenames are relatively simpler
 
179
    and therefore faster to perform than the fullpath patterns.
 
180
 
 
181
    Also, the extension patterns are more likely to find a match and
 
182
    so are matched first, then the basename patterns, then the fullpath
 
183
    patterns.
 
184
    """
 
185
    # We want to _add_patterns in a specific order (as per type_list below)
 
186
    # starting with the shortest and going to the longest.
 
187
    # As some Python version don't support ordered dicts the list below is
 
188
    # used to select inputs for _add_pattern in a specific order.
 
189
    pattern_types = [ "extension", "basename", "fullpath" ]
 
190
 
 
191
    pattern_info = {
 
192
        "extension" : {
 
193
            "translator" : _sub_extension,
 
194
            "prefix" : r'(?:.*/)?(?!.*/)(?:.*\.)'
 
195
        },
 
196
        "basename" : {
 
197
            "translator" : _sub_basename,
 
198
            "prefix" : r'(?:.*/)?(?!.*/)'
 
199
        },
 
200
        "fullpath" : {
 
201
            "translator" : _sub_fullpath,
 
202
            "prefix" : r''
 
203
        },
 
204
    }
 
205
 
 
206
    def __init__(self, patterns):
 
207
        self._regex_patterns = []
 
208
        pattern_lists = {
 
209
            "extension" : [],
 
210
            "basename" : [],
 
211
            "fullpath" : [],
 
212
        }
 
213
        for pat in patterns:
 
214
            pat = normalize_pattern(pat)
 
215
            pattern_lists[Globster.identify(pat)].append(pat)
 
216
        pi = Globster.pattern_info
 
217
        for t in Globster.pattern_types:
 
218
            self._add_patterns(pattern_lists[t], pi[t]["translator"],
 
219
                pi[t]["prefix"])
 
220
 
 
221
    def _add_patterns(self, patterns, translator, prefix=''):
 
222
        while patterns:
 
223
            grouped_rules = [
 
224
                '(%s)' % translator(pat) for pat in patterns[:99]]
 
225
            joined_rule = '%s(?:%s)$' % (prefix, '|'.join(grouped_rules))
 
226
            # Explicitly use lazy_compile here, because we count on its
 
227
            # nicer error reporting.
 
228
            self._regex_patterns.append((
 
229
                lazy_regex.lazy_compile(joined_rule, re.UNICODE),
 
230
                patterns[:99]))
 
231
            patterns = patterns[99:]
 
232
 
 
233
    def match(self, filename):
 
234
        """Searches for a pattern that matches the given filename.
 
235
 
 
236
        :return A matching pattern or None if there is no matching pattern.
 
237
        """
 
238
        try:
 
239
            for regex, patterns in self._regex_patterns:
 
240
                match = regex.match(filename)
 
241
                if match:
 
242
                    return patterns[match.lastindex -1]
 
243
        except errors.InvalidPattern, e:
 
244
            # We can't show the default e.msg to the user as thats for
 
245
            # the combined pattern we sent to regex. Instead we indicate to
 
246
            # the user that an ignore file needs fixing.
 
247
            mutter('Invalid pattern found in regex: %s.', e.msg)
 
248
            e.msg = "File ~/.bazaar/ignore or .bzrignore contains error(s)."
 
249
            bad_patterns = ''
 
250
            for _, patterns in self._regex_patterns:
 
251
                for p in patterns:
 
252
                    if not Globster.is_pattern_valid(p):
 
253
                        bad_patterns += ('\n  %s' % p)
 
254
            e.msg += bad_patterns
 
255
            raise e
 
256
        return None
 
257
 
 
258
    @staticmethod
 
259
    def identify(pattern):
 
260
        """Returns pattern category.
 
261
 
 
262
        :param pattern: normalized pattern.
 
263
        Identify if a pattern is fullpath, basename or extension
 
264
        and returns the appropriate type.
 
265
        """
 
266
        if pattern.startswith(u'RE:') or u'/' in pattern:
 
267
            return "fullpath"
 
268
        elif pattern.startswith(u'*.'):
 
269
            return "extension"
 
270
        else:
 
271
            return "basename"
 
272
 
 
273
    @staticmethod
 
274
    def is_pattern_valid(pattern):
 
275
        """Returns True if pattern is valid.
 
276
 
 
277
        :param pattern: Normalized pattern.
 
278
        is_pattern_valid() assumes pattern to be normalized.
 
279
        see: globbing.normalize_pattern
 
280
        """
 
281
        result = True
 
282
        translator = Globster.pattern_info[Globster.identify(pattern)]["translator"]
 
283
        tpattern = '(%s)' % translator(pattern)
 
284
        try:
 
285
            re_obj = lazy_regex.lazy_compile(tpattern, re.UNICODE)
 
286
            re_obj.search("") # force compile
 
287
        except errors.InvalidPattern, e:
 
288
            result = False
 
289
        return result
 
290
 
 
291
 
 
292
class ExceptionGlobster(object):
 
293
    """A Globster that supports exception patterns.
 
294
    
 
295
    Exceptions are ignore patterns prefixed with '!'.  Exception
 
296
    patterns take precedence over regular patterns and cause a 
 
297
    matching filename to return None from the match() function.  
 
298
    Patterns using a '!!' prefix are highest precedence, and act 
 
299
    as regular ignores. '!!' patterns are useful to establish ignores
 
300
    that apply under paths specified by '!' exception patterns.
 
301
    """
 
302
    
 
303
    def __init__(self,patterns):
 
304
        ignores = [[], [], []]
 
305
        for p in patterns:
 
306
            if p.startswith(u'!!'):
 
307
                ignores[2].append(p[2:])
 
308
            elif p.startswith(u'!'):
 
309
                ignores[1].append(p[1:])
 
310
            else:
 
311
                ignores[0].append(p)
 
312
        self._ignores = [Globster(i) for i in ignores]
 
313
        
 
314
    def match(self, filename):
 
315
        """Searches for a pattern that matches the given filename.
 
316
 
 
317
        :return A matching pattern or None if there is no matching pattern.
 
318
        """
 
319
        double_neg = self._ignores[2].match(filename)
 
320
        if double_neg:
 
321
            return "!!%s" % double_neg
 
322
        elif self._ignores[1].match(filename):
 
323
            return None
 
324
        else:
 
325
            return self._ignores[0].match(filename)
 
326
 
 
327
class _OrderedGlobster(Globster):
 
328
    """A Globster that keeps pattern order."""
 
329
 
 
330
    def __init__(self, patterns):
 
331
        """Constructor.
 
332
 
 
333
        :param patterns: sequence of glob patterns
 
334
        """
 
335
        # Note: This could be smarter by running like sequences together
 
336
        self._regex_patterns = []
 
337
        for pat in patterns:
 
338
            pat = normalize_pattern(pat)
 
339
            t = Globster.identify(pat)
 
340
            self._add_patterns([pat], Globster.pattern_info[t]["translator"],
 
341
                Globster.pattern_info[t]["prefix"])
 
342
 
 
343
 
 
344
_slashes = lazy_regex.lazy_compile(r'[\\/]+')
 
345
def normalize_pattern(pattern):
 
346
    """Converts backslashes in path patterns to forward slashes.
 
347
 
 
348
    Doesn't normalize regular expressions - they may contain escapes.
 
349
    """
 
350
    if not (pattern.startswith('RE:') or pattern.startswith('!RE:')):
 
351
        pattern = _slashes.sub('/', pattern)
 
352
    if len(pattern) > 1:
 
353
        pattern = pattern.rstrip('/')
 
354
    return pattern