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 
11 import logger = std.experimental.logger;
12 
13 @safe:
14 
15 /** Filter strings by first cutting out regions (include) and then selectively
16  * remove (exclude) from region.
17  *
18  * It assumes that if `include` is empty everything should match.
19  *
20  * I often use this in my programs to allow a user to specify what files to
21  * process and have some control over what to exclude.
22  *
23  * `--re-include` and `--re-exclude` is a suggestion for parameters to use with
24  * `getopt`.
25  */
26 struct ReFilter {
27     import std.regex : Regex, regex, matchFirst;
28 
29     Regex!char[] includeRe;
30     Regex!char[] excludeRe;
31 
32     /**
33      * The regular expressions are set to ignoring the case.
34      *
35      * Params:
36      *  include = regular expression.
37      *  exclude = regular expression.
38      */
39     this(string[] include, string[] exclude) {
40         foreach (r; include)
41             includeRe ~= regex(r, "i");
42         foreach (r; exclude)
43             excludeRe ~= regex(r, "i");
44     }
45 
46     /**
47      * Returns: true if `s` matches `ìncludeRe` and NOT matches any of `excludeRe`.
48      */
49     bool match(string s, void delegate(string s, string type) @safe logFailed = null) {
50         const inclPassed = () {
51             if (includeRe.empty)
52                 return true;
53             foreach (ref re; includeRe) {
54                 if (!matchFirst(s, re).empty)
55                     return true;
56             }
57             return false;
58         }();
59         if (!inclPassed) {
60             if (logFailed !is null)
61                 logFailed(s, "include");
62             return false;
63         }
64 
65         foreach (ref re; excludeRe) {
66             if (!matchFirst(s, re).empty) {
67                 if (logFailed !is null)
68                     logFailed(s, "exclude");
69                 return false;
70             }
71         }
72 
73         return true;
74     }
75 }
76 
77 /// Example:
78 unittest {
79     auto r = ReFilter(["foo.*"], [".*bar.*", ".*batman"]);
80     assert(["foo", "foobar", "foo smurf batman", "batman", "fo",
81             "foo mother"].filter!(a => r.match(a)).array == [
82             "foo", "foo mother"
83             ]);
84 }
85 
86 @("shall match everything by default")
87 unittest {
88     ReFilter r;
89     assert(["foo", "foobar"].filter!(a => r.match(a)).array == ["foo", "foobar"]);
90 }
91 
92 @("shall exclude the specified items")
93 unittest {
94     auto r = ReFilter(null, [".*bar.*", ".*batman"]);
95     assert(["foo", "foobar", "foo smurf batman", "batman", "fo",
96             "foo mother"].filter!(a => r.match(a)).array == [
97             "foo", "fo", "foo mother"
98             ]);
99 }
100 
101 /** Filter strings by first cutting out a region (include) and then selectively
102  * remove (exclude) from that region.
103  *
104  * I often use this in my programs to allow a user to specify what files to
105  * process and the have some control over what to exclude.
106  */
107 struct GlobFilter {
108     string[] include;
109     string[] exclude;
110 
111     /**
112      * The regular expressions are set to ignoring the case.
113      *
114      * Params:
115      *  include = glob string patter
116      *  exclude = glob string patterh
117      */
118     this(string[] include, string[] exclude) {
119         this.include = include;
120         this.exclude = exclude;
121     }
122 
123     /**
124      * Params:
125      *  logFailed = called when `s` fails matching.
126      *
127      * Returns: true if `s` matches `ìncludeRe` and NOT matches any of `excludeRe`.
128      */
129     bool match(string s, void delegate(string s, string[] filters) @safe logFailed = null) {
130         import std.algorithm : canFind;
131         import std.path : globMatch;
132 
133         if (!include.empty && !canFind!((a, b) => globMatch(b, a))(include, s)) {
134             if (logFailed !is null)
135                 logFailed(s, include);
136             return false;
137         }
138 
139         if (canFind!((a, b) => globMatch(b, a))(exclude, s)) {
140             if (logFailed !is null)
141                 logFailed(s, exclude);
142             return false;
143         }
144 
145         return true;
146     }
147 }
148 
149 /// Example:
150 unittest {
151     import std.algorithm : filter;
152     import std.array : array;
153 
154     auto r = GlobFilter(["foo*"], ["*bar*", "*batman"]);
155 
156     assert(["foo", "foobar", "foo smurf batman", "batman", "fo",
157             "foo mother"].filter!(a => r.match(a)).array == [
158             "foo", "foo mother"
159             ]);
160 }