-
-
Notifications
You must be signed in to change notification settings - Fork 116
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Pattern matching for the command line tool
- Loading branch information
Showing
7 changed files
with
363 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
jsign-cli/src/main/java/net/jsign/DirectoryScanner.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
/** | ||
* Copyright 2024 Emmanuel Bourg | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package net.jsign; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.nio.file.FileVisitResult; | ||
import java.nio.file.FileVisitor; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.attribute.BasicFileAttributes; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.StringTokenizer; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
/** | ||
* Scans a directory recursively and returns the files matching a pattern. | ||
* | ||
* @since 6.1 | ||
*/ | ||
class DirectoryScanner { | ||
|
||
/** | ||
* Scans the current directory for files matching the specified pattern. | ||
* | ||
* @param glob the glob pattern ({@code foo/**}{@code /*bar/*.exe}) | ||
*/ | ||
public List<Path> scan(String glob) throws IOException { | ||
// normalize the pattern | ||
glob = glob.replace('\\', '/').replace("/+", "/"); | ||
|
||
// adjust the base directory | ||
String basedir = findBaseDirectory(glob); | ||
|
||
// strip the base directory from the pattern | ||
glob = glob.substring(basedir.length()); | ||
Pattern pattern = Pattern.compile(globToRegExp(glob)); | ||
|
||
int maxDepth = maxPatternDepth(glob); | ||
|
||
// let's scan the files | ||
List<Path> matches = new ArrayList<>(); | ||
Files.walkFileTree(new File(basedir).toPath(), new FileVisitor<Path>() { | ||
private int depth = -1; | ||
|
||
@Override | ||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { | ||
String name = dir.getFileName().toString(); | ||
if (depth + 1 > maxDepth || ".svn".equals(name) || ".git".equals(name)) { | ||
return FileVisitResult.SKIP_SUBTREE; | ||
} else { | ||
depth++; | ||
return FileVisitResult.CONTINUE; | ||
} | ||
} | ||
|
||
@Override | ||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { | ||
String filename = file.toString(); | ||
filename = filename.replaceAll("\\\\", "/"); | ||
if (filename.startsWith("./")) { | ||
filename = filename.substring(2); | ||
} | ||
if (filename.startsWith(basedir)) { | ||
filename = filename.substring(basedir.length()); | ||
} | ||
if (pattern.matcher(filename).matches()) { | ||
matches.add(file); | ||
} | ||
|
||
return FileVisitResult.CONTINUE; | ||
} | ||
|
||
@Override | ||
public FileVisitResult visitFileFailed(Path file, IOException exc) { | ||
return FileVisitResult.CONTINUE; | ||
} | ||
|
||
@Override | ||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) { | ||
depth--; | ||
return FileVisitResult.CONTINUE; | ||
} | ||
}); | ||
|
||
return matches; | ||
} | ||
|
||
/** | ||
* Converts a glob pattern into a regular expression. | ||
* | ||
* @param glob the glob pattern to convert ({@code foo/**}{@code /bar/*.exe}) | ||
*/ | ||
String globToRegExp(String glob) { | ||
String delimiters = "/\\"; | ||
StringTokenizer tokenizer = new StringTokenizer(glob, delimiters, true); | ||
|
||
boolean ignoreNextSeparator = false; | ||
StringBuilder pattern = new StringBuilder(); | ||
while (tokenizer.hasMoreTokens()) { | ||
String token = tokenizer.nextToken(); | ||
if (token.length() == 1 && delimiters.contains(token)) { | ||
if (!ignoreNextSeparator) { | ||
pattern.append("/"); | ||
} | ||
} else if ("**".equals(token)){ | ||
pattern.append("(?:|.*/)"); | ||
ignoreNextSeparator = true; | ||
} else if (token.contains("*")) { | ||
ignoreNextSeparator = false; | ||
pattern.append("\\Q" + token.replaceAll("\\*", "\\\\E[^/]*\\\\Q") + "\\E"); | ||
} else { | ||
ignoreNextSeparator = false; | ||
pattern.append("\\Q").append(token).append("\\E"); | ||
} | ||
} | ||
|
||
return pattern.toString().replaceAll("/+", "/"); | ||
} | ||
|
||
/** | ||
* Finds the base directory of the specified pattern. | ||
*/ | ||
String findBaseDirectory(String pattern) { | ||
Pattern regexp = Pattern.compile("([^*]*/).*"); | ||
Matcher matcher = regexp.matcher(pattern); | ||
if (matcher.matches()) { | ||
return matcher.group(1); | ||
} else { | ||
return ""; | ||
} | ||
} | ||
|
||
/** | ||
* Returns the maximum depth of the pattern (stripped from its base directory). | ||
*/ | ||
int maxPatternDepth(String pattern) { | ||
if (pattern.contains("**")) { | ||
return 50; | ||
} | ||
|
||
int depth = 0; | ||
for (int i = 0; i < pattern.length(); i++) { | ||
if (pattern.charAt(i) == '/') { | ||
depth++; | ||
} | ||
} | ||
|
||
return depth; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
156 changes: 156 additions & 0 deletions
156
jsign-cli/src/test/java/net/jsign/DirectoryScannerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
/** | ||
* Copyright 2024 Emmanuel Bourg | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package net.jsign; | ||
|
||
import java.io.File; | ||
import java.nio.file.Path; | ||
import java.util.List; | ||
|
||
import org.junit.Test; | ||
|
||
import static org.junit.Assert.*; | ||
|
||
public class DirectoryScannerTest { | ||
|
||
@Test | ||
public void testGlobToRegExp() { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
assertEquals("RegExp for pattern *.exe", "\\Q\\E[^/]*\\Q.exe\\E", scanner.globToRegExp("*.exe")); | ||
assertEquals("RegExp for pattern build/*.exe", "\\Qbuild\\E/\\Q\\E[^/]*\\Q.exe\\E", scanner.globToRegExp("build/*.exe")); | ||
assertEquals("RegExp for pattern build/*.exe", "\\Qbuild\\E/\\Q\\E[^/]*\\Qapp\\E[^/]*\\Q.exe\\E", scanner.globToRegExp("build/*app*.exe")); | ||
assertEquals("RegExp for pattern build//*.exe", "\\Qbuild\\E/\\Q\\E[^/]*\\Q.exe\\E", scanner.globToRegExp("build//*.exe")); | ||
assertEquals("RegExp for pattern build\\*.exe", "\\Qbuild\\E/\\Q\\E[^/]*\\Q.exe\\E", scanner.globToRegExp("build\\*.exe")); | ||
assertEquals("RegExp for pattern build/**/package.msix", "\\Qbuild\\E/(?:|.*/)\\Qpackage.msix\\E", scanner.globToRegExp("build/**/package.msix")); | ||
assertEquals("RegExp for pattern build/**/artifacts/*.dll", "\\Qbuild\\E/(?:|.*/)\\Q\\E[^/]*\\Q.dll\\E", scanner.globToRegExp("build/**/*.dll")); | ||
} | ||
|
||
@Test | ||
public void testFindBaseDirectory() { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
assertEquals("Base directory for pattern ''", "", scanner.findBaseDirectory("")); | ||
assertEquals("Base directory for pattern *.exe", "", scanner.findBaseDirectory("*.exe")); | ||
assertEquals("Base directory for pattern **/*.exe", "", scanner.findBaseDirectory("**/*.exe")); | ||
assertEquals("Base directory for pattern /build/", "/build/", scanner.findBaseDirectory("/build/")); | ||
assertEquals("Base directory for pattern build/*.exe", "build/", scanner.findBaseDirectory("build/*.exe")); | ||
assertEquals("Base directory for pattern build/foo/**/bar/*.exe", "build/foo/", scanner.findBaseDirectory("build/foo/**/bar/*.exe")); | ||
assertEquals("Base directory for pattern ../../foo/bar*/*.dll", "../../foo/", scanner.findBaseDirectory("../../foo/bar*/*.dll")); | ||
assertEquals("Base directory for pattern ../../*foo*/bar*/*.dll", "../../", scanner.findBaseDirectory("../../*foo*/bar*/*.dll")); | ||
assertEquals("Base directory for pattern c:/dev/jsign/*.xml", "c:/dev/jsign/", scanner.findBaseDirectory("c:/dev/jsign/*.xml")); | ||
} | ||
|
||
@Test | ||
public void testMaxPatternDepth() { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
assertEquals("Max depth for pattern ''", 0, scanner.maxPatternDepth("")); | ||
assertEquals("Max depth for pattern *.exe", 0, scanner.maxPatternDepth("*.exe")); | ||
assertEquals("Max depth for pattern **/*.exe", 50, scanner.maxPatternDepth("**/*.exe")); | ||
assertEquals("Max depth for pattern build/*.exe", 1, scanner.maxPatternDepth("build/*.exe")); | ||
assertEquals("Max depth for pattern build/foo/**/bar/*.exe", 50, scanner.maxPatternDepth("build/foo/**/bar/*.exe")); | ||
assertEquals("Max depth for pattern foo/bar*/*.dll", 2, scanner.maxPatternDepth("foo/bar*/*.dll")); | ||
assertEquals("Max depth for pattern *foo*/bar*/*.dll", 2, scanner.maxPatternDepth("*foo*/bar*/*.dll")); | ||
} | ||
|
||
@Test | ||
public void testScanCurrentDirectory() throws Exception { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
List<Path> matches = scanner.scan("pom.xml"); | ||
|
||
assertEquals("number of matches", 1, matches.size()); | ||
assertEquals("match", new File("pom.xml"), matches.get(0).toFile()); | ||
} | ||
|
||
@Test | ||
public void testScanParentDirectory() throws Exception { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
List<Path> matches = scanner.scan("..\\pom.xml"); | ||
|
||
assertEquals("number of matches", 1, matches.size()); | ||
assertEquals("match", new File("../pom.xml"), matches.get(0).toFile()); | ||
} | ||
|
||
@Test | ||
public void testScanSubDirectory() throws Exception { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
List<Path> matches = scanner.scan("target/pom.xml"); | ||
|
||
assertEquals("number of matches", 0, matches.size()); | ||
} | ||
|
||
@Test | ||
public void testScanCurrentDirectoryWildcard() throws Exception { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
List<Path> matches = scanner.scan("*.xml"); | ||
|
||
assertEquals("number of matches", 1, matches.size()); | ||
assertEquals("match", new File("pom.xml"), matches.get(0).toFile()); | ||
} | ||
|
||
@Test | ||
public void testScanParentDirectoryWildcard() throws Exception { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
List<Path> matches = scanner.scan("..\\*.xml"); | ||
|
||
assertEquals("number of matches", 1, matches.size()); | ||
assertEquals("match", new File("../pom.xml"), matches.get(0).toFile()); | ||
} | ||
|
||
@Test | ||
public void testScanAbsoluteDirectoryWildcard() throws Exception { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
List<Path> matches = scanner.scan(new File("").getAbsolutePath() + "/*.xml"); | ||
|
||
assertEquals("number of matches", 1, matches.size()); | ||
assertEquals("match", new File(new File("").getAbsolutePath(), "pom.xml"), matches.get(0).toFile()); | ||
} | ||
|
||
@Test | ||
public void testScanCurrentDirectoryRecursively() throws Exception { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
List<Path> matches = scanner.scan("**/pom.xml"); | ||
|
||
assertEquals("number of matches", 1, matches.size()); | ||
assertEquals("match", new File("pom.xml"), matches.get(0).toFile()); | ||
} | ||
|
||
@Test | ||
public void testScanParentDirectoryRecursively() throws Exception { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
List<Path> matches = scanner.scan("../jsign-c*/**/pom.xml"); | ||
|
||
assertEquals("number of matches", 2, matches.size()); | ||
assertEquals("match", new File("../jsign-cli/pom.xml"), matches.get(0).toFile()); | ||
assertEquals("match", new File("../jsign-core/pom.xml"), matches.get(1).toFile()); | ||
} | ||
|
||
@Test | ||
public void testScanSubDirectoryRecursively() throws Exception { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
List<Path> matches = scanner.scan("../jsign-core/src/**/*.exe"); | ||
|
||
assertEquals("number of matches", 1, matches.size()); | ||
assertEquals("match", new File("../jsign-core/src/test/resources/wineyes.exe"), matches.get(0).toFile()); | ||
} | ||
|
||
@Test | ||
public void testScanAbsoluteDirectoryRecursively() throws Exception { | ||
DirectoryScanner scanner = new DirectoryScanner(); | ||
List<Path> matches = scanner.scan(new File("..").getCanonicalPath() + "/jsign-core/src/**/*.exe"); | ||
|
||
assertEquals("number of matches", 1, matches.size()); | ||
assertEquals("match", new File(new File("..").getCanonicalPath(), "jsign-core/src/test/resources/wineyes.exe"), matches.get(0).toFile()); | ||
} | ||
} |
Oops, something went wrong.