~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/globbing.py

  • Committer: Aaron Bentley
  • Date: 2006-07-11 15:04:57 UTC
  • mto: This revision was merged to the branch mainline in revision 1858.
  • Revision ID: abentley@panoramicfeedback.com-20060711150457-d4c96e9c60843973
Ensure commit respects file spec when committing removals

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