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结尾的字符串 | 
| ??? | 匹配所有由三个数字或字母构成字符串,如 aaaabcab1123 | 
| *[0-9]* | 匹配所有含有一个数字的字符串 | 
| *.{htm,html,pdf} | 匹配所有以 .htm.html 或.pdf结尾的字符串 | 
| a?*.java | 匹配所有以 a开头,且a之后至少由一个字母或数字,且以.java结尾的字符传 | 
| {foo*,*[0-9]*} | 匹配所有以 foo开头,或含有数字的字符串,如foobarx1yfoo1xyz | 
代码实例
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);
    }
}