Java 语言 Glob 语法规则
最近在做 Android
下应用专清的功能,需要检索微信、微博这类应用的垃圾文件,尝试了 apache
的 common-io
以及 java
的 nio
,最后使用 nio
的 Files.walkFileTree
遍历目录,并使用 PathMatcher
和 glob
规则做文件匹配,本文是对 glob
语法的总结。
什么是 Glob
Glob
是一种模式匹配,类似于正则表达式但是语法相对简单。Glob
语句是一个含有 *,?{}[]
这些特殊字符的字符串,并与目标字符串匹配。可以用来做文件路径匹配或文件查找。例如 Linux
下的命令 ls *.txt
,其中的 *.txt
就是一个 glob
语句。
语法规则
Java
语言中的 Glob
语法遵循几个简单的规则:
- 一个星号
*
匹配任意个数的字符,包括空。不包括路径边界 /
或 \
。例如 /path/*/abc
可以匹配 /path/a/abc
和 /path/b/abc
等。
- 两个星号
**
和一个星号类似,区别是可以跨路径边界,一般用来匹配多级目录。例如 /path/**/abc
可以用来匹配 /path/a/abc
、/path/b/abc
、/path/a/b/abc
、/path/a/b/c/abc
等。
- 一个问号
?
匹配任意一个字符
- 大括号
{}
大括号用来指定一个子模式匹配集合,例如:{sun,moon,stars}
可以匹配 sun
moon
starts
, {temp*, tmp*}
可以匹配任何以 temp
tmp
开头的字符串等。
- 方括号
[]
方括号表示匹配括号内的任意一个单个字符,当有 -
时表示匹配任意一个连续范围内的单个字符。例如:[aeiou]
匹配任意一个小写元音字符,[0-9]
匹配任意一个数字,[A-Z]
匹配任意一个大写字母,[a-z,A-Z]
匹配任意一个大写或小写字母。另外,在方括号内, *
?
\
字符仅匹配它们自身。
- 任意其它字符
任意的其他字符表示匹配它们自身。
- 反斜杠
\
转义
匹配 *
?
或其他特殊字符需要使用反斜杠\
转义。例如: \\
匹配一个反斜杠,\?
匹配一个问号。
举例
Glob | 说明 |
---|---|
*.html | 匹配所有 .html 结尾的字符串 |
??? | 匹配所有由三个数字或字母构成字符串,如 aaa abc ab1 123 |
*[0-9]* | 匹配所有含有一个数字的字符串 |
*.{htm,html,pdf} | 匹配所有以 .htm .html 或 .pdf 结尾的字符串 |
a?*.java | 匹配所有以 a 开头,且a 之后至少由一个字母或数字,且以 .java 结尾的字符传 |
{foo*,*[0-9]*} | 匹配所有以 foo 开头,或含有数字的字符串,如 foobar x1y foo1xyz |
代码实例
JDK
中 glob
相关的内容主要集中在 java.nio.file
包内,其中 glob
语法转正则表达式的实现在 sun.nio.fs.Globs.java
中。下面代码是一个检索 /sdcard/tencent
目录下所有 png
文件的例子:
String match = "glob:/sdcard/tencent/**/*.png";
System.out.println("scanning: " + match);
final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher(match);
SimpleFileVisitor<Path> fileVisitor = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
if (pathMatcher.matches(file)) {
System.out.println("find: " + file.toFile().toString())
}
return super.visitFile(file, attrs);
}
};
try {
Files.walkFileTree(Paths.get(dir.getPath()), fileVisitor);
} catch (IOException e) {
e.printStackTrace();
}
Glob 语句转正则表达式的实现
http://hg.openjdk.java.net/jdk7/jdk7/jdk/file/tip/src/share/classes/sun/nio/fs/Globs.java
package sun.nio.fs;
import java.util.regex.PatternSyntaxException;
public class Globs {
private Globs() { }
private static final String regexMetaChars = ".^$+{[]|()";
private static final String globMetaChars = "\\*?[{";
private static boolean isRegexMeta(char c) {
return regexMetaChars.indexOf(c) != -1;
}
private static boolean isGlobMeta(char c) {
return globMetaChars.indexOf(c) != -1;
}
private static char EOL = 0; //TBD
private static char next(String glob, int i) {
if (i < glob.length()) {
return glob.charAt(i);
}
return EOL;
}
/**
* Creates a regex pattern from the given glob expression.
*
* @throws PatternSyntaxException
*/
private static String toRegexPattern(String globPattern, boolean isDos) {
boolean inGroup = false;
StringBuilder regex = new StringBuilder("^");
int i = 0;
while (i < globPattern.length()) {
char c = globPattern.charAt(i++);
switch (c) {
case '\\':
// escape special characters
if (i == globPattern.length()) {
throw new PatternSyntaxException("No character to escape",
globPattern, i - 1);
}
char next = globPattern.charAt(i++);
if (isGlobMeta(next) || isRegexMeta(next)) {
regex.append('\\');
}
regex.append(next);
break;
case '/':
if (isDos) {
regex.append("\\\\");
} else {
regex.append(c);
}
break;
case '[':
// don't match name separator in class
if (isDos) {
regex.append("[[^\\\\]&&[");
} else {
regex.append("[[^/]&&[");
}
if (next(globPattern, i) == '^') {
// escape the regex negation char if it appears
regex.append("\\^");
i++;
} else {
// negation
if (next(globPattern, i) == '!') {
regex.append('^');
i++;
}
// hyphen allowed at start
if (next(globPattern, i) == '-') {
regex.append('-');
i++;
}
}
boolean hasRangeStart = false;
char last = 0;
while (i < globPattern.length()) {
c = globPattern.charAt(i++);
if (c == ']') {
break;
}
if (c == '/' || (isDos && c == '\\')) {
throw new PatternSyntaxException("Explicit 'name separator' in class",
globPattern, i - 1);
}
// TBD: how to specify ']' in a class?
if (c == '\\' || c == '[' ||
c == '&' && next(globPattern, i) == '&') {
// escape '\', '[' or "&&" for regex class
regex.append('\\');
}
regex.append(c);
if (c == '-') {
if (!hasRangeStart) {
throw new PatternSyntaxException("Invalid range",
globPattern, i - 1);
}
if ((c = next(globPattern, i++)) == EOL || c == ']') {
break;
}
if (c < last) {
throw new PatternSyntaxException("Invalid range",
globPattern, i - 3);
}
regex.append(c);
hasRangeStart = false;
} else {
hasRangeStart = true;
last = c;
}
}
if (c != ']') {
throw new PatternSyntaxException("Missing ']", globPattern, i - 1);
}
regex.append("]]");
break;
case '{':
if (inGroup) {
throw new PatternSyntaxException("Cannot nest groups",
globPattern, i - 1);
}
regex.append("(?:(?:");
inGroup = true;
break;
case '}':
if (inGroup) {
regex.append("))");
inGroup = false;
} else {
regex.append('}');
}
break;
case ',':
if (inGroup) {
regex.append(")|(?:");
} else {
regex.append(',');
}
break;
case '*':
if (next(globPattern, i) == '*') {
// crosses directory boundaries
regex.append(".*");
i++;
} else {
// within directory boundary
if (isDos) {
regex.append("[^\\\\]*");
} else {
regex.append("[^/]*");
}
}
break;
case '?':
if (isDos) {
regex.append("[^\\\\]");
} else {
regex.append("[^/]");
}
break;
default:
if (isRegexMeta(c)) {
regex.append('\\');
}
regex.append(c);
}
}
if (inGroup) {
throw new PatternSyntaxException("Missing '}", globPattern, i - 1);
}
return regex.append('$').toString();
}
static String toUnixRegexPattern(String globPattern) {
return toRegexPattern(globPattern, false);
}
static String toWindowsRegexPattern(String globPattern) {
return toRegexPattern(globPattern, true);
}
}