1 /**
2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 */
6 module my.filter;
7 
8 import std.algorithm : filter;
9 import std.array : array, empty;
10 import logger = std.experimental.logger;
11 
12 import my.optional;
13 import my.path;
14 
15 @safe:
16 
17 /** Filter strings by first cutting out regions (include) and then selectively
18  * remove (exclude) from region.
19  *
20  * It assumes that if `include` is empty everything should match.
21  *
22  * I often use this in my programs to allow a user to specify what files to
23  * process and have some control over what to exclude.
24  *
25  * `--re-include` and `--re-exclude` is a suggestion for parameters to use with
26  * `getopt`.
27  */
28 struct ReFilter {
29     import std.regex : Regex, regex, matchFirst;
30 
31     Regex!char[] includeRe;
32     Regex!char[] excludeRe;
33 
34     /**
35      * The regular expressions are set to ignoring the case.
36      *
37      * Params:
38      *  include = regular expression.
39      *  exclude = regular expression.
40      */
41     this(string[] include, string[] exclude) {
42         foreach (r; include)
43             includeRe ~= regex(r, "i");
44         foreach (r; exclude)
45             excludeRe ~= regex(r, "i");
46     }
47 
48     /**
49      * Returns: true if `s` matches `ìncludeRe` and NOT matches any of `excludeRe`.
50      */
51     bool match(string s, void delegate(string s, string type) @safe logFailed = null) {
52         const inclPassed = () {
53             if (includeRe.empty)
54                 return true;
55             foreach (ref re; includeRe) {
56                 if (!matchFirst(s, re).empty)
57                     return true;
58             }
59             return false;
60         }();
61         if (!inclPassed) {
62             if (logFailed !is null)
63                 logFailed(s, "include");
64             return false;
65         }
66 
67         foreach (ref re; excludeRe) {
68             if (!matchFirst(s, re).empty) {
69                 if (logFailed !is null)
70                     logFailed(s, "exclude");
71                 return false;
72             }
73         }
74 
75         return true;
76     }
77 }
78 
79 /// Example:
80 unittest {
81     auto r = ReFilter(["foo.*"], [".*bar.*", ".*batman"]);
82     assert(["foo", "foobar", "foo smurf batman", "batman", "fo",
83             "foo mother"].filter!(a => r.match(a)).array == [
84             "foo", "foo mother"
85             ]);
86 }
87 
88 @("shall match everything by default")
89 unittest {
90     ReFilter r;
91     assert(["foo", "foobar"].filter!(a => r.match(a)).array == ["foo", "foobar"]);
92 }
93 
94 @("shall exclude the specified items")
95 unittest {
96     auto r = ReFilter(null, [".*bar.*", ".*batman"]);
97     assert(["foo", "foobar", "foo smurf batman", "batman", "fo",
98             "foo mother"].filter!(a => r.match(a)).array == [
99             "foo", "fo", "foo mother"
100             ]);
101 }
102 
103 /** Filter strings by first cutting out a region (include) and then selectively
104  * remove (exclude) from that region.
105  *
106  * I often use this in my programs to allow a user to specify what files to
107  * process and the have some control over what to exclude.
108  */
109 struct GlobFilter {
110     string[] include;
111     string[] exclude;
112 
113     /**
114      * The regular expressions are set to ignoring the case.
115      *
116      * Params:
117      *  include = glob string patter
118      *  exclude = glob string patterh
119      */
120     this(string[] include, string[] exclude) {
121         this.include = include;
122         this.exclude = exclude;
123     }
124 
125     /**
126      * Params:
127      *  logFailed = called when `s` fails matching.
128      *
129      * Returns: true if `s` matches `ìncludeRe` and NOT matches any of `excludeRe`.
130      */
131     bool match(string s, void delegate(string s, string[] filters) @safe logFailed = null) {
132         import std.algorithm : canFind;
133         import std.path : globMatch;
134 
135         if (!include.empty && !canFind!((a, b) => globMatch(b, a))(include, s)) {
136             if (logFailed !is null)
137                 logFailed(s, include);
138             return false;
139         }
140 
141         if (canFind!((a, b) => globMatch(b, a))(exclude, s)) {
142             if (logFailed !is null)
143                 logFailed(s, exclude);
144             return false;
145         }
146 
147         return true;
148     }
149 }
150 
151 /// Example:
152 unittest {
153     import std.algorithm : filter;
154     import std.array : array;
155 
156     auto r = GlobFilter(["foo*"], ["*bar*", "*batman"]);
157 
158     assert(["foo", "foobar", "foo smurf batman", "batman", "fo",
159             "foo mother"].filter!(a => r.match(a)).array == [
160             "foo", "foo mother"
161             ]);
162 }
163 
164 @("shall not crash")
165 unittest {
166     auto r = GlobFilter(["*"], ["bar/*"]);
167 
168     assert(["foo", "bar", "bar/foo", "bar batman",].filter!(a => r.match(a))
169             .array == ["foo", "bar", "bar batman"]);
170 }
171 
172 GlobFilter merge(GlobFilter a, GlobFilter b) {
173     return GlobFilter(a.include ~ b.include, a.exclude ~ b.exclude);
174 }
175 
176 @("shall merge two filters")
177 unittest {
178     auto a = GlobFilter(["foo*"], ["*bar*", "*batman"]);
179     auto b = GlobFilter(["fun*"], ["*fun*"]);
180     auto c = merge(a, b);
181     assert(c.include == ["foo*", "fun*"]);
182     assert(c.exclude == ["*bar*", "*batman", "*fun*"]);
183 }
184 
185 struct GlobFilterClosestMatch {
186     GlobFilter filter;
187     AbsolutePath base;
188 
189     bool match(string s, void delegate(string s, string[] filters) @safe logFailed = null) {
190         import std.path : relativePath;
191 
192         return filter.match(s.relativePath(base.toString), logFailed);
193     }
194 }
195 
196 /** The closest matching filter.
197  *
198  * Use for example as:
199  * ```
200  * closest(filters, p).orElse(GlobFilterClosestMatch(defaultFilter, AbsolutePath("."))).match(p);
201  * ```
202  */
203 Optional!GlobFilterClosestMatch closest(GlobFilter[AbsolutePath] filters, AbsolutePath p) {
204     import std.path : rootName;
205 
206     if (filters.empty)
207         return typeof(return)(None.init);
208 
209     const root = p.toString.rootName;
210     while (p != root) {
211         p = p.dirName;
212         if (auto v = p in filters)
213             return some(GlobFilterClosestMatch(*v, p));
214     }
215 
216     return typeof(return)(None.init);
217 }