Show More
@@ -0,0 +1,91 | |||
|
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) |
@@ -0,0 +1,38 | |||
|
1 | from __future__ import absolute_import | |
|
2 | from __future__ import print_function | |
|
3 | ||
|
4 | import os | |
|
5 | import sys | |
|
6 | ||
|
7 | # make it runnable directly without run-tests.py | |
|
8 | sys.path[0:0] = [os.path.join(os.path.dirname(__file__), '..')] | |
|
9 | ||
|
10 | from mercurial import minifileset | |
|
11 | ||
|
12 | def check(text, truecases, falsecases): | |
|
13 | f = minifileset.compile(text) | |
|
14 | for args in truecases: | |
|
15 | if not f(*args): | |
|
16 | print('unexpected: %r should include %r' % (text, args)) | |
|
17 | for args in falsecases: | |
|
18 | if f(*args): | |
|
19 | print('unexpected: %r should exclude %r' % (text, args)) | |
|
20 | ||
|
21 | check('all()', [('a.php', 123), ('b.txt', 0)], []) | |
|
22 | check('none()', [], [('a.php', 123), ('b.txt', 0)]) | |
|
23 | check('!!!!((!(!!all())))', [], [('a.php', 123), ('b.txt', 0)]) | |
|
24 | ||
|
25 | check('"path:a" & (**.b | **.c)', [('a/b.b', 0), ('a/c.c', 0)], [('b/c.c', 0)]) | |
|
26 | check('("path:a" & **.b) | **.c', | |
|
27 | [('a/b.b', 0), ('a/c.c', 0), ('b/c.c', 0)], []) | |
|
28 | ||
|
29 | check('**.bin - size("<20B")', [('b.bin', 21)], [('a.bin', 11), ('b.txt', 21)]) | |
|
30 | ||
|
31 | check('!!**.bin or size(">20B") + "path:bin" or !size(">10")', | |
|
32 | [('a.bin', 11), ('b.txt', 21), ('bin/abc', 11)], | |
|
33 | [('a.notbin', 11), ('b.txt', 11), ('bin2/abc', 11)]) | |
|
34 | ||
|
35 | check('(**.php and size(">10KB")) | **.zip | ("path:bin" & !"path:bin/README") ' | |
|
36 | ' | size(">1M")', | |
|
37 | [('a.php', 15000), ('a.zip', 0), ('bin/a', 0), ('bin/README', 1e7)], | |
|
38 | [('a.php', 5000), ('b.zip2', 0), ('t/bin/a', 0), ('bin/README', 1)]) |
General Comments 0
You need to be logged in to leave comments.
Login now