Czy istnieje odpowiednik java.util.regex dla wzorców typu „glob”?

84

Czy istnieje standardowa (najlepiej Apache Commons lub podobna niewirusowa) biblioteka do dopasowywania typu „glob” w Javie? Kiedy raz musiałem zrobić coś podobnego w Perlu, po prostu zmieniłem wszystkie " ." na " \.", " *" na " .*" i " ?" na " ." i tym podobne rzeczy, ale zastanawiam się, czy ktoś zrobił Pracuj dla mnie.

Podobne pytanie: Utwórz wyrażenie regularne z wyrażenia glob

Paul Tomblin
źródło
GlobCompiler / GlobEngine z Jakarta ORO wygląda obiecująco. Jest dostępny na licencji Apache.
Steve Trout
Czy możesz podać dokładny przykład tego, co chcesz zrobić?
Thorbjørn Ravn Andersen
To, co chcę zrobić (a raczej to, co chce zrobić mój klient), to dopasować rzeczy takie jak „ -2009 /” lub „* rss ” w adresach URL. Przeważnie konwersja do wyrażenia regularnego jest dość trywialna, ale zastanawiałem się, czy istnieje łatwiejszy sposób.
Paul Tomblin
Polecam globowanie plików w stylu Ant, ponieważ wydaje się, że stało się kanonicznym globowaniem w świecie Java. Zobacz moją odpowiedź, aby uzyskać więcej informacji: stackoverflow.com/questions/1247772/… .
Adam Gent
1
@BradMace, powiązane, ale większość odpowiedzi zakłada, że ​​przechodzisz przez drzewo katalogów. Mimo to, jeśli ktoś nadal szuka sposobu dopasowania dowolnych ciągów w stylu glob, prawdopodobnie powinien również zajrzeć do tej odpowiedzi.
Paul Tomblin,

Odpowiedzi:

46

Nie ma nic wbudowanego, ale dość łatwo jest przekonwertować coś podobnego do globu na wyrażenie regularne:

public static String createRegexFromGlob(String glob)
{
    String out = "^";
    for(int i = 0; i < glob.length(); ++i)
    {
        final char c = glob.charAt(i);
        switch(c)
        {
        case '*': out += ".*"; break;
        case '?': out += '.'; break;
        case '.': out += "\\."; break;
        case '\\': out += "\\\\"; break;
        default: out += c;
        }
    }
    out += '$';
    return out;
}

to działa dla mnie, ale nie jestem pewien, czy obejmuje „standard” globu, jeśli taki istnieje :)

Aktualizacja autorstwa Paula Tomblina: Znalazłem program w Perlu, który wykonuje konwersję globów i dostosowuje go do języka Java.

    private String convertGlobToRegEx(String line)
    {
    LOG.info("got line [" + line + "]");
    line = line.trim();
    int strLen = line.length();
    StringBuilder sb = new StringBuilder(strLen);
    // Remove beginning and ending * globs because they're useless
    if (line.startsWith("*"))
    {
        line = line.substring(1);
        strLen--;
    }
    if (line.endsWith("*"))
    {
        line = line.substring(0, strLen-1);
        strLen--;
    }
    boolean escaping = false;
    int inCurlies = 0;
    for (char currentChar : line.toCharArray())
    {
        switch (currentChar)
        {
        case '*':
            if (escaping)
                sb.append("\\*");
            else
                sb.append(".*");
            escaping = false;
            break;
        case '?':
            if (escaping)
                sb.append("\\?");
            else
                sb.append('.');
            escaping = false;
            break;
        case '.':
        case '(':
        case ')':
        case '+':
        case '|':
        case '^':
        case '$':
        case '@':
        case '%':
            sb.append('\\');
            sb.append(currentChar);
            escaping = false;
            break;
        case '\\':
            if (escaping)
            {
                sb.append("\\\\");
                escaping = false;
            }
            else
                escaping = true;
            break;
        case '{':
            if (escaping)
            {
                sb.append("\\{");
            }
            else
            {
                sb.append('(');
                inCurlies++;
            }
            escaping = false;
            break;
        case '}':
            if (inCurlies > 0 && !escaping)
            {
                sb.append(')');
                inCurlies--;
            }
            else if (escaping)
                sb.append("\\}");
            else
                sb.append("}");
            escaping = false;
            break;
        case ',':
            if (inCurlies > 0 && !escaping)
            {
                sb.append('|');
            }
            else if (escaping)
                sb.append("\\,");
            else
                sb.append(",");
            break;
        default:
            escaping = false;
            sb.append(currentChar);
        }
    }
    return sb.toString();
}

Redaguję tę odpowiedź zamiast tworzyć własną, ponieważ ta odpowiedź stawia mnie na właściwej ścieżce.

Dave Ray
źródło
1
Tak, to prawie rozwiązanie, które wymyśliłem ostatnim razem, gdy musiałem to zrobić (w Perlu), ale zastanawiałem się, czy jest coś bardziej eleganckiego. Myślę, że zrobię to na swój sposób.
Paul Tomblin
1
Właściwie znalazłem lepszą implementację w Perlu, którą mogę zaadaptować do Javy na kobesearch.cpan.org/htdocs/Text-Glob/Text/Glob.pm.html
Paul Tomblin
Czy nie możesz użyć zamiany wyrażenia regularnego, aby zamienić glob w wyrażenie regularne?
Tim Sylvester
1
Linie na górze, które usuwają początkowy i końcowy znak „*”, muszą zostać usunięte z java, ponieważ String. dopasowuje się tylko do całego ciągu
KitsuneYMG
10
FYI: Standardem dla „globbingu” jest język POSIX Shell - opengroup.org/onlinepubs/009695399/utilities/ ...
Stephen C
60

Planowany jest również globbing zaimplementowany w Javie 7.

Zobacz FileSystem.getPathMatcher(String)i samouczek „Znajdowanie plików” .

finnw
źródło
23
Cudowny. Ale dlaczego, u licha, ta implementacja jest ograniczona do obiektów „Path”?!? W moim przypadku chcę dopasować URI ...
Yves Martin
3
Zaglądając do źródła sun.nio, dopasowywanie glob wydaje się być realizowane przez Globs.java . Niestety, jest to napisane specjalnie dla ścieżek systemu plików, więc nie może być używane dla wszystkich łańcuchów (przyjmuje pewne założenia dotyczące separatorów ścieżek i niedozwolonych znaków). Ale może to być pomocny punkt wyjścia.
Neil Traft,
33

Dziękuję wszystkim tutaj za ich wkład. Napisałem bardziej kompleksową konwersję niż którakolwiek z poprzednich odpowiedzi:

/**
 * Converts a standard POSIX Shell globbing pattern into a regular expression
 * pattern. The result can be used with the standard {@link java.util.regex} API to
 * recognize strings which match the glob pattern.
 * <p/>
 * See also, the POSIX Shell language:
 * http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_13_01
 * 
 * @param pattern A glob pattern.
 * @return A regex pattern to recognize the given glob pattern.
 */
public static final String convertGlobToRegex(String pattern) {
    StringBuilder sb = new StringBuilder(pattern.length());
    int inGroup = 0;
    int inClass = 0;
    int firstIndexInClass = -1;
    char[] arr = pattern.toCharArray();
    for (int i = 0; i < arr.length; i++) {
        char ch = arr[i];
        switch (ch) {
            case '\\':
                if (++i >= arr.length) {
                    sb.append('\\');
                } else {
                    char next = arr[i];
                    switch (next) {
                        case ',':
                            // escape not needed
                            break;
                        case 'Q':
                        case 'E':
                            // extra escape needed
                            sb.append('\\');
                        default:
                            sb.append('\\');
                    }
                    sb.append(next);
                }
                break;
            case '*':
                if (inClass == 0)
                    sb.append(".*");
                else
                    sb.append('*');
                break;
            case '?':
                if (inClass == 0)
                    sb.append('.');
                else
                    sb.append('?');
                break;
            case '[':
                inClass++;
                firstIndexInClass = i+1;
                sb.append('[');
                break;
            case ']':
                inClass--;
                sb.append(']');
                break;
            case '.':
            case '(':
            case ')':
            case '+':
            case '|':
            case '^':
            case '$':
            case '@':
            case '%':
                if (inClass == 0 || (firstIndexInClass == i && ch == '^'))
                    sb.append('\\');
                sb.append(ch);
                break;
            case '!':
                if (firstIndexInClass == i)
                    sb.append('^');
                else
                    sb.append('!');
                break;
            case '{':
                inGroup++;
                sb.append('(');
                break;
            case '}':
                inGroup--;
                sb.append(')');
                break;
            case ',':
                if (inGroup > 0)
                    sb.append('|');
                else
                    sb.append(',');
                break;
            default:
                sb.append(ch);
        }
    }
    return sb.toString();
}

I testy jednostkowe, aby udowodnić, że działa:

/**
 * @author Neil Traft
 */
public class StringUtils_ConvertGlobToRegex_Test {

    @Test
    public void star_becomes_dot_star() throws Exception {
        assertEquals("gl.*b", StringUtils.convertGlobToRegex("gl*b"));
    }

    @Test
    public void escaped_star_is_unchanged() throws Exception {
        assertEquals("gl\\*b", StringUtils.convertGlobToRegex("gl\\*b"));
    }

    @Test
    public void question_mark_becomes_dot() throws Exception {
        assertEquals("gl.b", StringUtils.convertGlobToRegex("gl?b"));
    }

    @Test
    public void escaped_question_mark_is_unchanged() throws Exception {
        assertEquals("gl\\?b", StringUtils.convertGlobToRegex("gl\\?b"));
    }

    @Test
    public void character_classes_dont_need_conversion() throws Exception {
        assertEquals("gl[-o]b", StringUtils.convertGlobToRegex("gl[-o]b"));
    }

    @Test
    public void escaped_classes_are_unchanged() throws Exception {
        assertEquals("gl\\[-o\\]b", StringUtils.convertGlobToRegex("gl\\[-o\\]b"));
    }

    @Test
    public void negation_in_character_classes() throws Exception {
        assertEquals("gl[^a-n!p-z]b", StringUtils.convertGlobToRegex("gl[!a-n!p-z]b"));
    }

    @Test
    public void nested_negation_in_character_classes() throws Exception {
        assertEquals("gl[[^a-n]!p-z]b", StringUtils.convertGlobToRegex("gl[[!a-n]!p-z]b"));
    }

    @Test
    public void escape_carat_if_it_is_the_first_char_in_a_character_class() throws Exception {
        assertEquals("gl[\\^o]b", StringUtils.convertGlobToRegex("gl[^o]b"));
    }

    @Test
    public void metachars_are_escaped() throws Exception {
        assertEquals("gl..*\\.\\(\\)\\+\\|\\^\\$\\@\\%b", StringUtils.convertGlobToRegex("gl?*.()+|^$@%b"));
    }

    @Test
    public void metachars_in_character_classes_dont_need_escaping() throws Exception {
        assertEquals("gl[?*.()+|^$@%]b", StringUtils.convertGlobToRegex("gl[?*.()+|^$@%]b"));
    }

    @Test
    public void escaped_backslash_is_unchanged() throws Exception {
        assertEquals("gl\\\\b", StringUtils.convertGlobToRegex("gl\\\\b"));
    }

    @Test
    public void slashQ_and_slashE_are_escaped() throws Exception {
        assertEquals("\\\\Qglob\\\\E", StringUtils.convertGlobToRegex("\\Qglob\\E"));
    }

    @Test
    public void braces_are_turned_into_groups() throws Exception {
        assertEquals("(glob|regex)", StringUtils.convertGlobToRegex("{glob,regex}"));
    }

    @Test
    public void escaped_braces_are_unchanged() throws Exception {
        assertEquals("\\{glob\\}", StringUtils.convertGlobToRegex("\\{glob\\}"));
    }

    @Test
    public void commas_dont_need_escaping() throws Exception {
        assertEquals("(glob,regex),", StringUtils.convertGlobToRegex("{glob\\,regex},"));
    }

}
Neil Traft
źródło
Dzięki za ten kod, Neil! Czy byłbyś skłonny dać mu licencję typu open source?
Steven
1
Wyrażam zgodę, aby kod w tej odpowiedzi był własnością publiczną.
Neil Traft
Powinienem zrobić coś jeszcze? :-P
Neil Traft
9

Istnieje kilka bibliotek, które wykonują dopasowywanie wzorców w stylu Glob, które są nowocześniejsze niż wymienione poniżej:

Tam Ants Directory Scanner and Springs AntPathMatcher

Polecam oba rozwiązania w porównaniu z innymi rozwiązaniami, ponieważ Ant Style Globbing stało się w zasadzie standardową składnią glob w świecie Javy (Hudson, Spring, Ant i myślę, że Maven).

Adam Gent
źródło
1
Oto współrzędne Maven dla artefaktu z AntPathMatcher: search.maven.org/ ... Oraz kilka testów z przykładowym użyciem: github.com/spring-projects/spring-framework/blob/master/…
seanf
I możesz dostosować znak „ścieżki” ... więc jest przydatny do rzeczy innych niż ścieżki ...
Michael Wiles
7

Niedawno musiałem to zrobić i użyłem \Qi \Euciec od wzorca glob:

private static Pattern getPatternFromGlob(String glob) {
  return Pattern.compile(
    "^" + Pattern.quote(glob)
            .replace("*", "\\E.*\\Q")
            .replace("?", "\\E.\\Q") 
    + "$");
}
Vincent Robert
źródło
4
Czy to się nie zepsuje, jeśli gdzieś w ciągu znajduje się \ E?
jmo
@jmo, tak, ale możesz to obejść, wstępnie przetwarzając globzmienną z glob = Pattern.quote (glob), która, jak sądzę, obsługuje takie skrajne przypadki. W takim przypadku nie musisz jednak poprzedzać i dołączać pierwszego i ostatniego \\ Q i \\ E.
Kimball Robinson
2
@jmo Poprawiłem przykład, aby użyć Pattern.quote ().
dimo414
5

To jest prosta implementacja Globu, która obsługuje * i? we wzorze

public class GlobMatch {
    private String text;
    private String pattern;

    public boolean match(String text, String pattern) {
        this.text = text;
        this.pattern = pattern;

        return matchCharacter(0, 0);
    }

    private boolean matchCharacter(int patternIndex, int textIndex) {
        if (patternIndex >= pattern.length()) {
            return false;
        }

        switch(pattern.charAt(patternIndex)) {
            case '?':
                // Match any character
                if (textIndex >= text.length()) {
                    return false;
                }
                break;

            case '*':
                // * at the end of the pattern will match anything
                if (patternIndex + 1 >= pattern.length() || textIndex >= text.length()) {
                    return true;
                }

                // Probe forward to see if we can get a match
                while (textIndex < text.length()) {
                    if (matchCharacter(patternIndex + 1, textIndex)) {
                        return true;
                    }
                    textIndex++;
                }

                return false;

            default:
                if (textIndex >= text.length()) {
                    return false;
                }

                String textChar = text.substring(textIndex, textIndex + 1);
                String patternChar = pattern.substring(patternIndex, patternIndex + 1);

                // Note the match is case insensitive
                if (textChar.compareToIgnoreCase(patternChar) != 0) {
                    return false;
                }
        }

        // End of pattern and text?
        if (patternIndex + 1 >= pattern.length() && textIndex + 1 >= text.length()) {
            return true;
        }

        // Go on to match the next character in the pattern
        return matchCharacter(patternIndex + 1, textIndex + 1);
    }
}
Tony Edgecombe
źródło
5

Podobny do Tony Edgecombe „s odpowiedź , tutaj jest krótka i prosta globber że podpory *i ?bez użycia regex, jeśli ktoś potrzebuje jednego.

public static boolean matches(String text, String glob) {
    String rest = null;
    int pos = glob.indexOf('*');
    if (pos != -1) {
        rest = glob.substring(pos + 1);
        glob = glob.substring(0, pos);
    }

    if (glob.length() > text.length())
        return false;

    // handle the part up to the first *
    for (int i = 0; i < glob.length(); i++)
        if (glob.charAt(i) != '?' 
                && !glob.substring(i, i + 1).equalsIgnoreCase(text.substring(i, i + 1)))
            return false;

    // recurse for the part after the first *, if any
    if (rest == null) {
        return glob.length() == text.length();
    } else {
        for (int i = glob.length(); i <= text.length(); i++) {
            if (matches(text.substring(i), rest))
                return true;
        }
        return false;
    }
}
mihi
źródło
1
Doskonała odpowiedź tihi! Jest to na tyle proste, aby można było to szybko zrozumieć, a jednocześnie niezbyt oszałamiające :-)
Limited Atonement
3

To może być nieco hakerskie podejście. Rozgryzłem to z Files.newDirectoryStream(Path dir, String glob)kodu NIO2 . Zwróć uwagę, że każde dopasowanie nowego Pathobiektu jest tworzone. Do tej pory mogłem to przetestować tylko na Windows FS, jednak uważam, że powinno to działać również na Uniksie.

// a file system hack to get a glob matching
PathMatcher matcher = ("*".equals(glob)) ? null
    : FileSystems.getDefault().getPathMatcher("glob:" + glob);

if ("*".equals(glob) || matcher.matches(Paths.get(someName))) {
    // do you stuff here
}

UPDATE Działa na obu - Mac i Linux.

Andrii Karaivanskyi
źródło
0

Dawno temu robiłem ogromne filtrowanie tekstu sterowane globalnie, więc napisałem mały fragment kodu (15 linii kodu, bez zależności poza JDK). Obsługuje tylko „*” (dla mnie było wystarczające), ale można go łatwo rozszerzyć na „?”. Jest kilka razy szybszy niż wstępnie skompilowane wyrażenie regularne, nie wymaga żadnej wstępnej kompilacji (zasadniczo jest to porównanie ciągów ze znakami za każdym razem, gdy dopasowywany jest wzorzec).

Kod:

  public static boolean miniglob(String[] pattern, String line) {
    if (pattern.length == 0) return line.isEmpty();
    else if (pattern.length == 1) return line.equals(pattern[0]);
    else {
      if (!line.startsWith(pattern[0])) return false;
      int idx = pattern[0].length();
      for (int i = 1; i < pattern.length - 1; ++i) {
        String patternTok = pattern[i];
        int nextIdx = line.indexOf(patternTok, idx);
        if (nextIdx < 0) return false;
        else idx = nextIdx + patternTok.length();
      }
      if (!line.endsWith(pattern[pattern.length - 1])) return false;
      return true;
    }
  }

Stosowanie:

  public static void main(String[] args) {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    try {
      // read from stdin space separated text and pattern
      for (String input = in.readLine(); input != null; input = in.readLine()) {
        String[] tokens = input.split(" ");
        String line = tokens[0];
        String[] pattern = tokens[1].split("\\*+", -1 /* want empty trailing token if any */);

        // check matcher performance
        long tm0 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; ++i) {
          miniglob(pattern, line);
        }
        long tm1 = System.currentTimeMillis();
        System.out.println("miniglob took " + (tm1-tm0) + " ms");

        // check regexp performance
        Pattern reptn = Pattern.compile(tokens[1].replace("*", ".*"));
        Matcher mtchr = reptn.matcher(line);
        tm0 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; ++i) {
          mtchr.matches();
        }
        tm1 = System.currentTimeMillis();
        System.out.println("regexp took " + (tm1-tm0) + " ms");

        // check if miniglob worked correctly
        if (miniglob(pattern, line)) {
          System.out.println("+ >" + line);
        }
        else {
          System.out.println("- >" + line);
        }
      }
    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }

Skopiuj / wklej stąd

bobah
źródło
Ponieważ jest to tylko 15 wierszy, należy je uwzględnić tutaj na wypadek, gdyby połączona strona przestała działać.
Raniz,
0

Poprzednie rozwiązanie przez Vincent Robert / dimo414 opiera się na Pattern.quote()realizowany w kategoriach\Q ... \E, który nie jest udokumentowany w API i dlatego nie może być w przypadku innych przyszłych / implementacji Javy. Poniższe rozwiązanie usuwa tę zależność implementacji, zmieniając znaczenie wszystkich wystąpień \Ezamiast używania quote(). Aktywuje także DOTALLtryb ( (?s)) w przypadku, gdy dopasowywany ciąg zawiera znaki nowej linii.

    public static Pattern globToRegex(String glob)
    {
        return Pattern.compile(
            "(?s)^\\Q" +
            glob.replace("\\E", "\\E\\\\E\\Q")
                .replace("*", "\\E.*\\Q")
                .replace("?", "\\E.\\Q") +
            "\\E$"
        );
    }
nmatt
źródło
-1

Nawiasem mówiąc, wygląda na to, że zrobiłeś to na własnej skórze w Perlu

To załatwia sprawę w Perlu:

my @files = glob("*.html")
# Or, if you prefer:
my @files = <*.html> 

źródło
1
Działa to tylko wtedy, gdy glob służy do dopasowywania plików. W przypadku Perla, globy faktycznie pochodziły z listy adresów IP, która została napisana przy użyciu globów z powodów, do których nie będę się odnosił, aw moim obecnym przypadku globy miały pasować do adresów URL.
Paul Tomblin