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 }