├── .classpath ├── .gitignore ├── .project ├── .settings ├── org.eclipse.core.resources.prefs └── org.eclipse.jdt.core.prefs ├── LICENSE ├── README.md ├── project.yaml └── src └── com └── esotericsoftware └── wildcard ├── GlobScanner.java ├── Paths.java └── RegexScanner.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary Files 2 | *~ 3 | .*.swp 4 | .DS_STORE 5 | 6 | bin/ 7 | target/ 8 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | wildcard 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding/=Cp1252 3 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled 3 | org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore 4 | org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull 5 | org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault 6 | org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable 7 | org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled 8 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 9 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 10 | org.eclipse.jdt.core.compiler.compliance=1.8 11 | org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning 12 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 13 | org.eclipse.jdt.core.compiler.problem.autoboxing=ignore 14 | org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning 15 | org.eclipse.jdt.core.compiler.problem.deadCode=ignore 16 | org.eclipse.jdt.core.compiler.problem.deprecation=ignore 17 | org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled 18 | org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled 19 | org.eclipse.jdt.core.compiler.problem.discouragedReference=warning 20 | org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore 21 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 22 | org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore 23 | org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore 24 | org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled 25 | org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore 26 | org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning 27 | org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning 28 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 29 | org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning 30 | org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled 31 | org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning 32 | org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning 33 | org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore 34 | org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore 35 | org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning 36 | org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore 37 | org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore 38 | org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled 39 | org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore 40 | org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore 41 | org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled 42 | org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore 43 | org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore 44 | org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning 45 | org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning 46 | org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore 47 | org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning 48 | org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error 49 | org.eclipse.jdt.core.compiler.problem.nullReference=warning 50 | org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error 51 | org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning 52 | org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning 53 | org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore 54 | org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning 55 | org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore 56 | org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore 57 | org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore 58 | org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning 59 | org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore 60 | org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore 61 | org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore 62 | org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore 63 | org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore 64 | org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled 65 | org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning 66 | org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled 67 | org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled 68 | org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled 69 | org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=warning 70 | org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning 71 | org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled 72 | org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=ignore 73 | org.eclipse.jdt.core.compiler.problem.unclosedCloseable=ignore 74 | org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore 75 | org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning 76 | org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore 77 | org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore 78 | org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore 79 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore 80 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled 81 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled 82 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=enabled 83 | org.eclipse.jdt.core.compiler.problem.unusedImport=ignore 84 | org.eclipse.jdt.core.compiler.problem.unusedLabel=warning 85 | org.eclipse.jdt.core.compiler.problem.unusedLocal=ignore 86 | org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore 87 | org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore 88 | org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled 89 | org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=enabled 90 | org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=enabled 91 | org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=ignore 92 | org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore 93 | org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning 94 | org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning 95 | org.eclipse.jdt.core.compiler.release=enabled 96 | org.eclipse.jdt.core.compiler.source=1.8 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Nathan Sweet 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://raw.github.com/wiki/EsotericSoftware/wildcard/images/logo.png) 2 | 3 | Please use the [WildCard discussion group](http://groups.google.com/group/wildcard-users) for support. 4 | 5 | Wildcard is a small Java library that performs efficient pattern matching of files and directories. Paths can be matched with wildcards or regular expressions. Matched files can be easily copied, deleted, zipped, etc. 6 | 7 | ## Glob matching 8 | 9 | The `glob` method collects files and directories using literal characters and optional wildcards: 10 | 11 | Paths paths = new Paths(); 12 | paths.glob("/some/directory", "resources"); 13 | paths.glob("/some/directory", "images/**/*.jpg", "!**/.svn/**"); 14 | 15 | The first parameter defines the root directory of the search. Subsequent parameters are a variable number of search patterns. The following wildcards are supported in search patterns: 16 | 17 | 18 | 19 | 20 | 21 | 22 |
?Matches any single character. Eg, "something?" collects any path that is named "something" plus any character.
*Matches any characters up to the next slash. Eg, "*/*/something*" collects any path that has two directories, then a file or directory that starts with the name "something".
**Matches any characters. Eg, "**/something/**" collects any path that contains a directory named "something".
!A pattern starting with an exclamation point (!) causes paths matched by the pattern to be excluded, even if other patterns would select the paths.
23 | 24 | When using `glob`, the search is done as efficiently as possible. Directories are not traversed if none of the search patterns can match them. 25 | 26 | Glob is also used when constructor parameters are specified: 27 | 28 | Paths paths = new Paths("/some/directory", "resources"); 29 | 30 | ## Regex matching 31 | 32 | Regular expressions can be used to collect files and directories: 33 | 34 | Paths paths = new Paths(); 35 | paths.regex("/some/directory", "images.*\\.jpg", "!.*/\\.svn/.*"); 36 | 37 | Regex patterns that being with `!` caused matched paths to be excluded, even if other patterns would select the paths. 38 | 39 | Regular expressions have more expressive power, but it comes at a price. When using `regex`, the search is not done as efficiently as with `glob`. All directories and files under the root are traversed, even if none of the search patterns can match them. 40 | 41 | ## Pipe delimited patterns 42 | 43 | If `glob` or `regex` is passed only one parameter, it may be a root directory and then any number of search patterns, delimited by pipe (|) characters: 44 | 45 | Paths paths = new Paths(); 46 | paths.glob("/some/directory|resources"); 47 | paths.glob("/some/directory|images/**/*.jpg|!**/.svn/**"); 48 | 49 | This is useful in cases where it is more convenient to use a single string to describe what files to collect. 50 | 51 | If `glob` is passed only one parameter that is not pipe delimited, or if only exclude patterns are specified (using the `!` character), then an additional search pattern of `**` is implied. 52 | 53 | ## Utility methods 54 | 55 | The `glob` and `regex` methods can be called repeatedly to collect paths from different root directories. Internally, a `Paths` instance holds all the paths matched and remembers each root directory where the search was performed. This greatly simplifies many tasks. The `Paths` class has utility methods for manipulating the paths, eg: 56 | 57 | Paths paths = new Paths(); 58 | paths.glob("/some/directory", "**/*.jpg"); 59 | paths.copyTo("/another/directory"); 60 | 61 | This collects all JPG files in any directory under "/some/directory". It then copies those files to "/another/directory". Note that the directory structure under the root directory is preserved. Eg, if you had these files: 62 | 63 | /some/directory/stuff.jpg 64 | /some/directory/otherstuff.gif 65 | /some/directory/animals/cat.jpg 66 | /some/directory/animals/dog.jpg 67 | /some/directory/animals/giraffe.tga 68 | 69 | The result after the copy would be: 70 | 71 | /another/directory/stuff.jpg 72 | /another/directory/animals/cat.jpg 73 | /another/directory/animals/dog.jpg 74 | 75 | The `Paths` class has methods to copy, delete, and zip the paths. It also has methods to obtain the individual paths in various ways, so you can take whatever action you like: 76 | 77 | for (String fullPath : new Paths(".", "*.png")) { ... } 78 | for (String dirName : new Paths(".").dirsOnly().getNames()) { ... } 79 | for (File file : new Paths(".", "*.jpg").getFiles()) { ... ) 80 | -------------------------------------------------------------------------------- /project.yaml: -------------------------------------------------------------------------------- 1 | main: com.esotericsoftware.wildcard.Paths 2 | version: 1.04 3 | -------------------------------------------------------------------------------- /src/com/esotericsoftware/wildcard/GlobScanner.java: -------------------------------------------------------------------------------- 1 | 2 | package com.esotericsoftware.wildcard; 3 | 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | 10 | class GlobScanner { 11 | private final File rootDir; 12 | private final List matches = new ArrayList(128); 13 | 14 | public GlobScanner (File rootDir, List includes, List excludes, boolean ignoreCase) { 15 | if (rootDir == null) throw new IllegalArgumentException("rootDir cannot be null."); 16 | if (!rootDir.exists()) throw new IllegalArgumentException("Directory does not exist: " + rootDir); 17 | if (!rootDir.isDirectory()) throw new IllegalArgumentException("File must be a directory: " + rootDir); 18 | try { 19 | rootDir = rootDir.getCanonicalFile(); 20 | } catch (IOException ex) { 21 | throw new RuntimeException("OS error determining canonical path: " + rootDir, ex); 22 | } 23 | this.rootDir = rootDir; 24 | 25 | if (includes == null) throw new IllegalArgumentException("includes cannot be null."); 26 | if (excludes == null) throw new IllegalArgumentException("excludes cannot be null."); 27 | 28 | if (includes.isEmpty()) includes.add("**"); 29 | List includePatterns = new ArrayList(includes.size()); 30 | for (String include : includes) 31 | includePatterns.add(new Pattern(include, ignoreCase)); 32 | 33 | List allExcludePatterns = new ArrayList(excludes.size()); 34 | for (String exclude : excludes) 35 | allExcludePatterns.add(new Pattern(exclude, ignoreCase)); 36 | 37 | scanDir(rootDir, includePatterns); 38 | 39 | if (!allExcludePatterns.isEmpty()) { 40 | // For each file, see if any exclude patterns match. 41 | outerLoop: 42 | // 43 | for (Iterator matchIter = matches.iterator(); matchIter.hasNext();) { 44 | String filePath = (String)matchIter.next(); 45 | List excludePatterns = new ArrayList(allExcludePatterns); 46 | try { 47 | // Shortcut for excludes that are "**/XXX", just check file name. 48 | for (Iterator excludeIter = excludePatterns.iterator(); excludeIter.hasNext();) { 49 | Pattern exclude = (Pattern)excludeIter.next(); 50 | if (exclude.values.length == 2 && exclude.values[0].equals("**")) { 51 | exclude.incr(); 52 | String fileName = filePath.substring(filePath.lastIndexOf(File.separatorChar) + 1); 53 | if (exclude.matchesFile(fileName)) { 54 | matchIter.remove(); 55 | continue outerLoop; 56 | } 57 | excludeIter.remove(); 58 | } 59 | } 60 | // Get the file names after the root dir. 61 | String[] fileNames = filePath.split("\\" + File.separator); 62 | for (String fileName : fileNames) { 63 | for (Iterator excludeIter = excludePatterns.iterator(); excludeIter.hasNext();) { 64 | Pattern exclude = (Pattern)excludeIter.next(); 65 | if (!exclude.matchesFile(fileName)) { 66 | excludeIter.remove(); 67 | continue; 68 | } 69 | exclude.incr(fileName); 70 | if (exclude.wasFinalMatch()) { 71 | // Exclude pattern matched. 72 | matchIter.remove(); 73 | continue outerLoop; 74 | } 75 | } 76 | // Stop processing the file if none of the exclude patterns matched. 77 | if (excludePatterns.isEmpty()) continue outerLoop; 78 | } 79 | } finally { 80 | for (Pattern exclude : allExcludePatterns) 81 | exclude.reset(); 82 | } 83 | } 84 | } 85 | } 86 | 87 | private void scanDir (File dir, List includes) { 88 | if (!dir.canRead()) return; 89 | 90 | // See if patterns are specific enough to avoid scanning every file in the directory. 91 | boolean scanAll = false; 92 | for (Pattern include : includes) { 93 | if (include.value.indexOf('*') != -1 || include.value.indexOf('?') != -1) { 94 | scanAll = true; 95 | break; 96 | } 97 | } 98 | 99 | if (!scanAll) { 100 | // If not scanning all the files, we know exactly which ones to include. 101 | List matchingIncludes = new ArrayList(1); 102 | for (Pattern include : includes) { 103 | if (matchingIncludes.isEmpty()) 104 | matchingIncludes.add(include); 105 | else 106 | matchingIncludes.set(0, include); 107 | process(dir, include.value, matchingIncludes); 108 | } 109 | } else { 110 | // Scan every file. 111 | String[] fileNames = dir.list(); 112 | if (fileNames == null) return; 113 | for (String fileName : fileNames) { 114 | // Get all include patterns that match. 115 | List matchingIncludes = new ArrayList(includes.size()); 116 | for (Pattern include : includes) 117 | if (include.matchesFile(fileName)) matchingIncludes.add(include); 118 | if (matchingIncludes.isEmpty()) continue; 119 | process(dir, fileName, matchingIncludes); 120 | } 121 | } 122 | } 123 | 124 | private void process (File dir, String fileName, List matchingIncludes) { 125 | // Increment patterns that need to move to the next token. 126 | boolean isFinalMatch = false; 127 | List incrementedPatterns = new ArrayList(); 128 | for (Iterator iter = matchingIncludes.iterator(); iter.hasNext();) { 129 | Pattern include = (Pattern)iter.next(); 130 | if (include.incr(fileName)) { 131 | incrementedPatterns.add(include); 132 | if (include.isExhausted()) iter.remove(); 133 | } 134 | if (include.wasFinalMatch()) isFinalMatch = true; 135 | } 136 | 137 | File file = new File(dir, fileName); 138 | if (isFinalMatch) { 139 | int length = rootDir.getPath().length(); 140 | if (!rootDir.getPath().endsWith(File.separator)) length++; // Lose starting slash. 141 | matches.add(file.getPath().substring(length)); 142 | } 143 | if (!matchingIncludes.isEmpty() && file.isDirectory()) scanDir(file, matchingIncludes); 144 | 145 | // Decrement patterns. 146 | for (Pattern include : incrementedPatterns) 147 | include.decr(); 148 | } 149 | 150 | public List matches () { 151 | return matches; 152 | } 153 | 154 | public File rootDir () { 155 | return rootDir; 156 | } 157 | 158 | static class Pattern { 159 | String value; 160 | boolean ignoreCase; 161 | final String[] values; 162 | 163 | private int index; 164 | 165 | Pattern (String pattern, boolean ignoreCase) { 166 | this.ignoreCase = ignoreCase; 167 | 168 | pattern = pattern.replace('\\', '/'); 169 | pattern = pattern.replaceAll("\\*\\*(?=[^/])", "**/*"); 170 | pattern = pattern.replaceAll("(?<=[^/])\\*\\*", "*/**"); 171 | if (ignoreCase) pattern = pattern.toLowerCase(); 172 | 173 | values = pattern.split("/"); 174 | value = values[0]; 175 | } 176 | 177 | boolean matchesPath (String path) { 178 | reset(); 179 | String[] files = path.split("[\\\\/]"); 180 | for (int i = 0, n = files.length; i < n; i++) { 181 | String file = files[i]; 182 | if (i > 0 && isExhausted()) return false; 183 | if (!matchesFile(file)) return false; 184 | if (!incr(file) && isExhausted()) return true; 185 | } 186 | return wasFinalMatch(); 187 | } 188 | 189 | boolean matchesFile (String fileName) { 190 | String value = this.value; 191 | if (value.equals("**")) return true; 192 | 193 | if (ignoreCase) fileName = fileName.toLowerCase(); 194 | 195 | // Shortcut if no wildcards. 196 | if (value.indexOf('*') == -1 && value.indexOf('?') == -1) return fileName.equals(value); 197 | 198 | int i = 0, j = 0, fileNameLength = fileName.length(), valueLength = value.length(); 199 | while (i < fileNameLength && j < valueLength) { 200 | char c = value.charAt(j); 201 | if (c == '*') break; 202 | if (c != '?' && c != fileName.charAt(i)) return false; 203 | i++; 204 | j++; 205 | } 206 | 207 | // If reached end of pattern without finding a * wildcard, the match has to fail if not same length. 208 | if (j == valueLength) return fileNameLength == valueLength; 209 | 210 | int cp = 0; 211 | int mp = 0; 212 | while (i < fileNameLength) { 213 | if (j < valueLength) { 214 | char c = value.charAt(j); 215 | if (c == '*') { 216 | if (j++ >= valueLength) return true; 217 | mp = j; 218 | cp = i + 1; 219 | continue; 220 | } 221 | if (c == '?' || c == fileName.charAt(i)) { 222 | j++; 223 | i++; 224 | continue; 225 | } 226 | } 227 | j = mp; 228 | i = cp++; 229 | } 230 | 231 | // Handle trailing asterisks. 232 | while (j < valueLength && value.charAt(j) == '*') 233 | j++; 234 | 235 | return j >= valueLength; 236 | } 237 | 238 | String nextValue () { 239 | if (index + 1 == values.length) return null; 240 | return values[index + 1]; 241 | } 242 | 243 | boolean incr (String fileName) { 244 | if (value.equals("**")) { 245 | if (index == values.length - 1) return false; 246 | incr(); 247 | if (matchesFile(fileName)) 248 | incr(); 249 | else { 250 | decr(); 251 | return false; 252 | } 253 | } else 254 | incr(); 255 | return true; 256 | } 257 | 258 | void incr () { 259 | index++; 260 | if (index >= values.length) 261 | value = null; 262 | else 263 | value = values[index]; 264 | } 265 | 266 | void decr () { 267 | index--; 268 | if (index > 0 && values[index - 1].equals("**")) index--; 269 | value = values[index]; 270 | } 271 | 272 | void reset () { 273 | index = 0; 274 | value = values[0]; 275 | } 276 | 277 | boolean isExhausted () { 278 | return index >= values.length; 279 | } 280 | 281 | boolean isLast () { 282 | return index >= values.length - 1; 283 | } 284 | 285 | boolean wasFinalMatch () { 286 | return isExhausted() || (isLast() && value.equals("**")); 287 | } 288 | } 289 | 290 | public static void main (String[] args) { 291 | List includes = new ArrayList(); 292 | includes.add("src/**.java"); 293 | List excludes = new ArrayList(); 294 | long start = System.nanoTime(); 295 | List files = new GlobScanner(new File("."), includes, excludes, false).matches(); 296 | long end = System.nanoTime(); 297 | System.out.println(files.toString().replaceAll(", ", "\n").replaceAll("[\\[\\]]", "")); 298 | System.out.println((end - start) / 1000000f); 299 | System.out.println(files); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/com/esotericsoftware/wildcard/Paths.java: -------------------------------------------------------------------------------- 1 | 2 | package com.esotericsoftware.wildcard; 3 | 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.nio.channels.FileChannel; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.Comparator; 13 | import java.util.HashSet; 14 | import java.util.Iterator; 15 | import java.util.List; 16 | import java.util.zip.Deflater; 17 | import java.util.zip.ZipEntry; 18 | import java.util.zip.ZipOutputStream; 19 | 20 | /** Collects filesystem paths using wildcards, preserving the directory structure. Copies, deletes, and zips paths. */ 21 | public class Paths implements Iterable { 22 | static private final Comparator LONGEST_TO_SHORTEST = new Comparator() { 23 | public int compare (Path s1, Path s2) { 24 | return s2.absolute().length() - s1.absolute().length(); 25 | } 26 | }; 27 | 28 | static private List defaultGlobExcludes; 29 | 30 | final HashSet paths = new HashSet(32); 31 | 32 | /** Creates an empty Paths object. */ 33 | public Paths () { 34 | } 35 | 36 | /** Creates a Paths object and calls {@link #glob(String, String[])} with the specified arguments. */ 37 | public Paths (String dir, String... patterns) { 38 | glob(dir, patterns); 39 | } 40 | 41 | /** Creates a Paths object and calls {@link #glob(String, List)} with the specified arguments. */ 42 | public Paths (String dir, List patterns) { 43 | glob(dir, patterns); 44 | } 45 | 46 | private Paths glob (String dir, boolean ignoreCase, String... patterns) { 47 | if (dir == null) dir = "."; 48 | if (patterns != null && patterns.length == 0) { 49 | String[] split = dir.split("\\|"); 50 | if (split.length > 1) { 51 | dir = split[0]; 52 | patterns = new String[split.length - 1]; 53 | System.arraycopy(split, 1, patterns, 0, split.length - 1); 54 | } 55 | } 56 | File dirFile = new File(dir); 57 | if (!dirFile.exists()) return this; 58 | 59 | List includes = new ArrayList(); 60 | List excludes = new ArrayList(); 61 | if (patterns != null) { 62 | for (String pattern : patterns) { 63 | if (pattern.length() == 0) throw new IllegalArgumentException("Pattern may not be empty."); 64 | if (pattern.charAt(0) == '!') 65 | excludes.add(pattern.substring(1)); 66 | else 67 | includes.add(pattern); 68 | } 69 | } 70 | if (includes.isEmpty()) includes.add("**"); 71 | 72 | if (defaultGlobExcludes != null) excludes.addAll(defaultGlobExcludes); 73 | 74 | GlobScanner scanner = new GlobScanner(dirFile, includes, excludes, ignoreCase); 75 | String rootDir = scanner.rootDir().getPath().replace('\\', '/'); 76 | if (!rootDir.endsWith("/")) rootDir += '/'; 77 | for (String filePath : scanner.matches()) 78 | paths.add(new Path(rootDir, filePath)); 79 | return this; 80 | } 81 | 82 | /** Collects all files and directories in the specified directory matching the wildcard patterns. 83 | * @param dir The directory containing the paths to collect. If it does not exist, no paths are collected. If null, "." is 84 | * assumed. 85 | * @param patterns The wildcard patterns of the paths to collect or exclude. Patterns may optionally contain wildcards 86 | * represented by asterisks and question marks. If empty or omitted then the dir parameter is split on the "|" 87 | * character, the first element is used as the directory and remaining are used as the patterns. If null, ** is 88 | * assumed (collects all paths).
89 | *
90 | * A single question mark (?) matches any single character. Eg, something? collects any path that is named 91 | * "something" plus any character.
92 | *
93 | * A single asterisk (*) matches any characters up to the next slash (/). Eg, *\*\something* collects any path that 94 | * has two directories of any name, then a file or directory that starts with the name "something".
95 | *
96 | * A double asterisk (**) matches any characters. Eg, **\something\** collects any path that contains a directory 97 | * named "something".
98 | *
99 | * A pattern starting with an exclamation point (!) causes paths matched by the pattern to be excluded, even if other 100 | * patterns would select the paths. */ 101 | public Paths glob (String dir, String... patterns) { 102 | return glob(dir, false, patterns); 103 | } 104 | 105 | /** Case insensitive glob. 106 | * @see #glob(String, String...) */ 107 | public Paths globIgnoreCase (String dir, String... patterns) { 108 | return glob(dir, true, patterns); 109 | } 110 | 111 | /** Case sensitive glob. 112 | * @see #glob(String, String...) */ 113 | public Paths glob (String dir, List patterns) { 114 | if (patterns == null) throw new IllegalArgumentException("patterns cannot be null."); 115 | glob(dir, false, patterns.toArray(new String[patterns.size()])); 116 | return this; 117 | } 118 | 119 | /** Case insensitive glob. 120 | * @see #glob(String, String...) */ 121 | public Paths globIgnoreCase (String dir, List patterns) { 122 | if (patterns == null) throw new IllegalArgumentException("patterns cannot be null."); 123 | glob(dir, true, patterns.toArray(new String[patterns.size()])); 124 | return this; 125 | } 126 | 127 | /** Collects all files and directories in the specified directory matching the regular expression patterns. This method is much 128 | * slower than {@link #glob(String, String...)} because every file and directory under the specified directory must be 129 | * inspected. 130 | * @param dir The directory containing the paths to collect. If it does not exist, no paths are collected. 131 | * @param patterns The regular expression patterns of the paths to collect or exclude. If empty or omitted then the dir 132 | * parameter is split on the "|" character, the first element is used as the directory and remaining are used as the 133 | * patterns. If null, ** is assumed (collects all paths).
134 | *
135 | * A pattern starting with an exclamation point (!) causes paths matched by the pattern to be excluded, even if other 136 | * patterns would select the paths. */ 137 | public Paths regex (String dir, String... patterns) { 138 | if (dir == null) dir = "."; 139 | if (patterns != null && patterns.length == 0) { 140 | String[] split = dir.split("\\|"); 141 | if (split.length > 1) { 142 | dir = split[0]; 143 | patterns = new String[split.length - 1]; 144 | System.arraycopy(split, 1, patterns, 0, split.length - 1); 145 | } 146 | } 147 | File dirFile = new File(dir); 148 | if (!dirFile.exists()) return this; 149 | 150 | List includes = new ArrayList(); 151 | List excludes = new ArrayList(); 152 | if (patterns != null) { 153 | for (String pattern : patterns) { 154 | if (pattern.length() == 0) throw new IllegalArgumentException("Pattern may not be empty."); 155 | if (pattern.charAt(0) == '!') 156 | excludes.add(pattern.substring(1)); 157 | else 158 | includes.add(pattern); 159 | } 160 | } 161 | if (includes.isEmpty()) includes.add(".*"); 162 | 163 | RegexScanner scanner = new RegexScanner(dirFile, includes, excludes); 164 | String rootDir = scanner.rootDir().getPath().replace('\\', '/'); 165 | if (!rootDir.endsWith("/")) rootDir += '/'; 166 | for (String filePath : scanner.matches()) 167 | paths.add(new Path(rootDir, filePath)); 168 | return this; 169 | } 170 | 171 | /** Copies the files and directories to the specified directory. 172 | * @return A paths object containing the paths of the new files. */ 173 | public Paths copyTo (String destDir) throws IOException { 174 | Paths newPaths = new Paths(); 175 | for (Path path : paths) { 176 | File destFile = new File(destDir, path.name); 177 | File srcFile = path.file(); 178 | if (srcFile.isDirectory()) { 179 | destFile.mkdirs(); 180 | } else { 181 | destFile.getParentFile().mkdirs(); 182 | copyFile(srcFile, destFile); 183 | } 184 | newPaths.paths.add(new Path(destDir, path.name)); 185 | } 186 | return newPaths; 187 | } 188 | 189 | /** Deletes all the files, directories, and any files in the directories. 190 | * @return False if any file could not be deleted. */ 191 | public boolean delete () { 192 | boolean success = true; 193 | List pathsCopy = new ArrayList(paths); 194 | Collections.sort(pathsCopy, LONGEST_TO_SHORTEST); 195 | for (File file : getFiles(pathsCopy)) { 196 | if (file.isDirectory()) { 197 | if (!deleteDirectory(file)) success = false; 198 | } else { 199 | if (!file.delete()) success = false; 200 | } 201 | } 202 | return success; 203 | } 204 | 205 | /** Compresses the files and directories specified by the paths into a new zip file at the specified location. If there are no 206 | * paths or all the paths are directories, no zip file will be created. */ 207 | public void zip (String destFile) throws IOException { 208 | Paths zipPaths = filesOnly(); 209 | if (zipPaths.paths.isEmpty()) return; 210 | byte[] buf = new byte[1024]; 211 | ZipOutputStream out = new ZipOutputStream(new FileOutputStream(destFile)); 212 | out.setLevel(Deflater.BEST_COMPRESSION); 213 | try { 214 | for (Path path : zipPaths.paths) { 215 | File file = path.file(); 216 | out.putNextEntry(new ZipEntry(path.name.replace('\\', '/'))); 217 | FileInputStream in = new FileInputStream(file); 218 | int bytesRead; 219 | while ((bytesRead = in.read(buf)) > 0) 220 | out.write(buf, 0, bytesRead); 221 | in.close(); 222 | out.closeEntry(); 223 | } 224 | } finally { 225 | out.close(); 226 | } 227 | } 228 | 229 | public int count () { 230 | return paths.size(); 231 | } 232 | 233 | public boolean isEmpty () { 234 | return paths.isEmpty(); 235 | } 236 | 237 | /** Returns the absolute paths delimited by the specified character. */ 238 | public String toString (String delimiter) { 239 | StringBuffer buffer = new StringBuffer(256); 240 | for (String path : getPaths()) { 241 | if (buffer.length() > 0) buffer.append(delimiter); 242 | buffer.append(path); 243 | } 244 | return buffer.toString(); 245 | } 246 | 247 | /** Returns the absolute paths delimited by commas. */ 248 | public String toString () { 249 | return toString(", "); 250 | } 251 | 252 | /** Returns a Paths object containing the paths that are files, as if each file were selected from its parent directory. */ 253 | public Paths flatten () { 254 | Paths newPaths = new Paths(); 255 | for (Path path : paths) { 256 | File file = path.file(); 257 | if (file.isFile()) newPaths.paths.add(new Path(file.getParent(), file.getName())); 258 | } 259 | return newPaths; 260 | } 261 | 262 | /** Returns a Paths object containing the paths that are files. */ 263 | public Paths filesOnly () { 264 | Paths newPaths = new Paths(); 265 | for (Path path : paths) 266 | if (path.file().isFile()) newPaths.paths.add(path); 267 | return newPaths; 268 | } 269 | 270 | /** Returns a Paths object containing the paths that are directories. */ 271 | public Paths dirsOnly () { 272 | Paths newPaths = new Paths(); 273 | for (Path path : paths) 274 | if (path.file().isDirectory()) newPaths.paths.add(path); 275 | return newPaths; 276 | } 277 | 278 | /** Returns the paths as File objects. */ 279 | public List getFiles () { 280 | return getFiles(new ArrayList(paths)); 281 | } 282 | 283 | private ArrayList getFiles (List paths) { 284 | ArrayList files = new ArrayList(paths.size()); 285 | for (Path path : paths) 286 | files.add(path.file()); 287 | return files; 288 | } 289 | 290 | /** Returns the portion of the path after the root directory where the path was collected. */ 291 | public List getRelativePaths () { 292 | ArrayList stringPaths = new ArrayList(paths.size()); 293 | for (Path path : paths) 294 | stringPaths.add(path.name); 295 | return stringPaths; 296 | } 297 | 298 | /** Returns the full paths. */ 299 | public List getPaths () { 300 | ArrayList stringPaths = new ArrayList(paths.size()); 301 | for (File file : getFiles()) 302 | stringPaths.add(file.getPath()); 303 | return stringPaths; 304 | } 305 | 306 | /** Returns the paths' filenames. */ 307 | public List getNames () { 308 | ArrayList stringPaths = new ArrayList(paths.size()); 309 | for (File file : getFiles()) 310 | stringPaths.add(file.getName()); 311 | return stringPaths; 312 | } 313 | 314 | /** Adds a single path to this Paths object. */ 315 | public Paths addFile (String fullPath) { 316 | File file = new File(fullPath); 317 | String parent = file.getParent(); 318 | paths.add(new Path(parent == null ? "" : parent, file.getName())); 319 | return this; 320 | } 321 | 322 | /** Adds a single path to this Paths object. */ 323 | public Paths add (String dir, String name) { 324 | paths.add(new Path(dir, name)); 325 | return this; 326 | } 327 | 328 | /** Adds all paths from the specified Paths object to this Paths object. */ 329 | public void add (Paths paths) { 330 | this.paths.addAll(paths.paths); 331 | } 332 | 333 | /** Iterates over the absolute paths. The iterator supports the remove method. */ 334 | public Iterator iterator () { 335 | return new Iterator() { 336 | private Iterator iter = paths.iterator(); 337 | 338 | public void remove () { 339 | iter.remove(); 340 | } 341 | 342 | public String next () { 343 | return iter.next().absolute(); 344 | } 345 | 346 | public boolean hasNext () { 347 | return iter.hasNext(); 348 | } 349 | }; 350 | } 351 | 352 | /** Iterates over the paths as File objects. The iterator supports the remove method. */ 353 | public Iterator fileIterator () { 354 | return new Iterator() { 355 | private Iterator iter = paths.iterator(); 356 | 357 | public void remove () { 358 | iter.remove(); 359 | } 360 | 361 | public File next () { 362 | return iter.next().file(); 363 | } 364 | 365 | public boolean hasNext () { 366 | return iter.hasNext(); 367 | } 368 | }; 369 | } 370 | 371 | static private final class Path { 372 | public final String dir; 373 | public final String name; 374 | 375 | public Path (String dir, String name) { 376 | if (dir.length() > 0 && !dir.endsWith("/")) dir += "/"; 377 | this.dir = dir; 378 | this.name = name; 379 | } 380 | 381 | public String absolute () { 382 | return dir + name; 383 | } 384 | 385 | public File file () { 386 | return new File(dir, name); 387 | } 388 | 389 | public int hashCode () { 390 | final int prime = 31; 391 | int result = 1; 392 | result = prime * result + ((dir == null) ? 0 : dir.hashCode()); 393 | result = prime * result + ((name == null) ? 0 : name.hashCode()); 394 | return result; 395 | } 396 | 397 | public boolean equals (Object obj) { 398 | if (this == obj) return true; 399 | if (obj == null) return false; 400 | if (getClass() != obj.getClass()) return false; 401 | Path other = (Path)obj; 402 | if (dir == null) { 403 | if (other.dir != null) return false; 404 | } else if (!dir.equals(other.dir)) return false; 405 | if (name == null) { 406 | if (other.name != null) return false; 407 | } else if (!name.equals(other.name)) return false; 408 | return true; 409 | } 410 | } 411 | 412 | /** Sets the exclude patterns that will be used in addition to the excludes specified for all glob searches. */ 413 | static public void setDefaultGlobExcludes (String... defaultGlobExcludes) { 414 | Paths.defaultGlobExcludes = Arrays.asList(defaultGlobExcludes); 415 | } 416 | 417 | /** Copies one file to another. */ 418 | static private void copyFile (File in, File out) throws IOException { 419 | FileInputStream sourceStream = new FileInputStream(in); 420 | FileOutputStream destinationStream = new FileOutputStream(out); 421 | FileChannel sourceChannel = sourceStream.getChannel(); 422 | FileChannel destinationChannel = destinationStream.getChannel(); 423 | sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel); 424 | sourceChannel.close(); 425 | sourceStream.close(); 426 | destinationChannel.close(); 427 | destinationStream.close(); 428 | } 429 | 430 | /** Deletes a directory and all files and directories it contains. */ 431 | static private boolean deleteDirectory (File file) { 432 | if (file.exists()) { 433 | File[] files = file.listFiles(); 434 | for (int i = 0, n = files.length; i < n; i++) { 435 | if (files[i].isDirectory()) 436 | deleteDirectory(files[i]); 437 | else 438 | files[i].delete(); 439 | } 440 | } 441 | return file.delete(); 442 | } 443 | 444 | static public void main (String[] args) throws Exception { 445 | System.out.println(new Paths().glob("C:\\|test**|meow")); 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /src/com/esotericsoftware/wildcard/RegexScanner.java: -------------------------------------------------------------------------------- 1 | 2 | package com.esotericsoftware.wildcard; 3 | 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | import java.util.regex.Pattern; 10 | 11 | class RegexScanner { 12 | private final File rootDir; 13 | private final List includePatterns; 14 | private final List matches = new ArrayList(128); 15 | 16 | public RegexScanner (File rootDir, List includes, List excludes) { 17 | if (rootDir == null) throw new IllegalArgumentException("rootDir cannot be null."); 18 | if (!rootDir.exists()) throw new IllegalArgumentException("Directory does not exist: " + rootDir); 19 | if (!rootDir.isDirectory()) throw new IllegalArgumentException("File must be a directory: " + rootDir); 20 | try { 21 | rootDir = rootDir.getCanonicalFile(); 22 | } catch (IOException ex) { 23 | throw new RuntimeException("OS error determining canonical path: " + rootDir, ex); 24 | } 25 | this.rootDir = rootDir; 26 | 27 | if (includes == null) throw new IllegalArgumentException("includes cannot be null."); 28 | if (excludes == null) throw new IllegalArgumentException("excludes cannot be null."); 29 | 30 | includePatterns = new ArrayList(); 31 | for (String include : includes) 32 | includePatterns.add(Pattern.compile(include, Pattern.CASE_INSENSITIVE)); 33 | 34 | List excludePatterns = new ArrayList(); 35 | for (String exclude : excludes) 36 | excludePatterns.add(Pattern.compile(exclude, Pattern.CASE_INSENSITIVE)); 37 | 38 | scanDir(rootDir); 39 | 40 | for (Iterator matchIter = matches.iterator(); matchIter.hasNext();) { 41 | String filePath = (String)matchIter.next(); 42 | for (Pattern exclude : excludePatterns) 43 | if (exclude.matcher(filePath).matches()) matchIter.remove(); 44 | } 45 | } 46 | 47 | private void scanDir (File dir) { 48 | for (File file : dir.listFiles()) { 49 | for (Pattern include : includePatterns) { 50 | int length = rootDir.getPath().length(); 51 | if (!rootDir.getPath().endsWith(File.separator)) length++; // Lose starting slash. 52 | String filePath = file.getPath().substring(length); 53 | if (include.matcher(filePath).matches()) { 54 | matches.add(filePath); 55 | break; 56 | } 57 | } 58 | if (file.isDirectory()) scanDir(file); 59 | } 60 | } 61 | 62 | public List matches () { 63 | return matches; 64 | } 65 | 66 | public File rootDir () { 67 | return rootDir; 68 | } 69 | 70 | public static void main (String[] args) { 71 | // System.out.println(new Paths("C:\\Java\\ls", "**")); 72 | List includes = new ArrayList(); 73 | includes.add("core[^T]+php"); 74 | // includes.add(".*/lavaserver/.*"); 75 | List excludes = new ArrayList(); 76 | // excludes.add("website/**/doc**"); 77 | long start = System.nanoTime(); 78 | List files = new RegexScanner(new File("..\\website\\includes"), includes, excludes).matches(); 79 | long end = System.nanoTime(); 80 | System.out.println(files.toString().replaceAll(", ", "\n").replaceAll("[\\[\\]]", "")); 81 | System.out.println((end - start) / 1000000f); 82 | } 83 | } 84 | --------------------------------------------------------------------------------