Java 语言 Glob 语法规则

发布于:

编程

最近在做 Android 下应用专清的功能,需要检索微信、微博这类应用的垃圾文件,尝试了 apachecommon-io 以及 javanio ,最后使用 nioFiles.walkFileTree 遍历目录,并使用 PathMatcherglob 规则做文件匹配,本文是对 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

代码实例

JDKglob 相关的内容主要集中在 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);
    }
}

参考

Glob with Java NIO

Finding Files

What Is a Glob?

glob (programming)