|
1 # minifileset.py - a simple language to select files |
|
2 # |
|
3 # Copyright 2017 Facebook, Inc. |
|
4 # |
|
5 # This software may be used and distributed according to the terms of the |
|
6 # GNU General Public License version 2 or any later version. |
|
7 |
|
8 from __future__ import absolute_import |
|
9 |
|
10 from .i18n import _ |
|
11 from . import ( |
|
12 error, |
|
13 fileset, |
|
14 ) |
|
15 |
|
16 def _compile(tree): |
|
17 if not tree: |
|
18 raise error.ParseError(_("missing argument")) |
|
19 op = tree[0] |
|
20 if op == 'symbol': |
|
21 name = fileset.getstring(tree, _('invalid file pattern')) |
|
22 if name.startswith('**'): # file extension test, ex. "**.tar.gz" |
|
23 ext = name[2:] |
|
24 for c in ext: |
|
25 if c in '*{}[]?/\\': |
|
26 raise error.ParseError(_('reserved character: %s') % c) |
|
27 return lambda n, s: n.endswith(ext) |
|
28 raise error.ParseError(_('invalid symbol: %s') % name) |
|
29 elif op == 'string': |
|
30 # TODO: teach fileset about 'path:', so that this can be a symbol and |
|
31 # not require quoting. |
|
32 name = fileset.getstring(tree, _('invalid path literal')) |
|
33 if name.startswith('path:'): # directory or full path test |
|
34 p = name[5:] # prefix |
|
35 pl = len(p) |
|
36 f = lambda n, s: n.startswith(p) and (len(n) == pl or n[pl] == '/') |
|
37 return f |
|
38 raise error.ParseError(_("invalid string"), |
|
39 hint=_('paths must be prefixed with "path:"')) |
|
40 elif op == 'or': |
|
41 func1 = _compile(tree[1]) |
|
42 func2 = _compile(tree[2]) |
|
43 return lambda n, s: func1(n, s) or func2(n, s) |
|
44 elif op == 'and': |
|
45 func1 = _compile(tree[1]) |
|
46 func2 = _compile(tree[2]) |
|
47 return lambda n, s: func1(n, s) and func2(n, s) |
|
48 elif op == 'not': |
|
49 return lambda n, s: not _compile(tree[1])(n, s) |
|
50 elif op == 'group': |
|
51 return _compile(tree[1]) |
|
52 elif op == 'func': |
|
53 symbols = { |
|
54 'all': lambda n, s: True, |
|
55 'none': lambda n, s: False, |
|
56 'size': lambda n, s: fileset.sizematcher(tree[2])(s), |
|
57 } |
|
58 |
|
59 x = tree[1] |
|
60 name = x[1] |
|
61 if x[0] == 'symbol' and name in symbols: |
|
62 return symbols[name] |
|
63 |
|
64 raise error.UnknownIdentifier(name, symbols.keys()) |
|
65 elif op == 'minus': # equivalent to 'x and not y' |
|
66 func1 = _compile(tree[1]) |
|
67 func2 = _compile(tree[2]) |
|
68 return lambda n, s: func1(n, s) and not func2(n, s) |
|
69 elif op == 'negate': |
|
70 raise error.ParseError(_("can't use negate operator in this context")) |
|
71 elif op == 'list': |
|
72 raise error.ParseError(_("can't use a list in this context"), |
|
73 hint=_('see hg help "filesets.x or y"')) |
|
74 raise error.ProgrammingError('illegal tree: %r' % (tree,)) |
|
75 |
|
76 def compile(text): |
|
77 """generate a function (path, size) -> bool from filter specification. |
|
78 |
|
79 "text" could contain the operators defined by the fileset language for |
|
80 common logic operations, and parenthesis for grouping. The supported path |
|
81 tests are '**.extname' for file extension test, and '"path:dir/subdir"' |
|
82 for prefix test. The ``size()`` predicate is borrowed from filesets to test |
|
83 file size. The predicates ``all()`` and ``none()`` are also supported. |
|
84 |
|
85 '(**.php & size(">10MB")) | **.zip | ("path:bin" & !"path:bin/README")' for |
|
86 example, will catch all php files whose size is greater than 10 MB, all |
|
87 files whose name ends with ".zip", and all files under "bin" in the repo |
|
88 root except for "bin/README". |
|
89 """ |
|
90 tree = fileset.parse(text) |
|
91 return _compile(tree) |