Gibt es ein Äquivalent von java.util.regex für Muster vom Typ "Glob"?

84

Gibt es eine Standardbibliothek (vorzugsweise Apache Commons oder eine ähnlich nicht virale Bibliothek), um Übereinstimmungen vom Typ "Glob" in Java durchzuführen? Als ich in Perl einmal Ähnliches tun musste, habe ich einfach alle " ." in " \.", die " *" in " .*" und die " ?" in " ." und so etwas geändert , aber ich frage mich, ob jemand das getan hat arbeite für mich.

Ähnliche Frage: Erstellen Sie einen regulären Ausdruck aus dem Glob-Ausdruck

Paul Tomblin
quelle
GlobCompiler / GlobEngine von Jakarta ORO sieht vielversprechend aus. Es ist unter der Apache-Lizenz verfügbar.
Steve Trout
Können Sie ein genaues Beispiel dafür geben, was Sie tun möchten?
Thorbjørn Ravn Andersen
Was ich tun möchte (oder besser gesagt, was mein Kunde tun möchte), ist Dinge wie " -2009 /" oder "* rss " in URLs abzugleichen. Meistens ist es ziemlich trivial, in Regex umzuwandeln, aber ich habe mich gefragt, ob es einen einfacheren Weg gibt.
Paul Tomblin
Ich empfehle das Globing von Dateien im Ant-Stil, da es anscheinend das kanonische Globing in der Java-Welt geworden ist. Weitere Informationen finden Sie in meiner Antwort: stackoverflow.com/questions/1247772/… .
Adam Gent
1
@BradMace, verwandt, aber die meisten Antworten gehen davon aus, dass Sie einen Verzeichnisbaum durchlaufen. Wenn jedoch immer noch jemand nach einer Möglichkeit sucht, eine beliebige Zeichenfolge im Glob-Stil abzugleichen, sollte er wahrscheinlich auch in dieser Antwort nachsehen.
Paul Tomblin

Antworten:

46

Es ist nichts eingebaut, aber es ist ziemlich einfach, etwas Glob-ähnliches in einen regulären Ausdruck umzuwandeln:

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;
}

Das funktioniert bei mir, aber ich bin mir nicht sicher, ob es den globalen "Standard" abdeckt, wenn es einen gibt :)

Update von Paul Tomblin: Ich habe ein Perl-Programm gefunden, das die Glob-Konvertierung durchführt und es an Java anpasst. Am Ende habe ich:

    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();
}

Ich bearbeite diese Antwort, anstatt meine eigene zu erstellen, weil diese Antwort mich auf den richtigen Weg gebracht hat.

Dave Ray
quelle
1
Ja, das ist so ziemlich die Lösung, die ich beim letzten Mal gefunden habe (in Perl), aber ich habe mich gefragt, ob es etwas eleganteres gibt. Ich denke, ich werde es auf deine Weise tun.
Paul Tomblin
1
Tatsächlich habe ich in Perl eine bessere Implementierung gefunden, die ich unter kobesearch.cpan.org/htdocs/Text-Glob/Text/Glob.pm.html
Paul Tomblin
Könnten Sie nicht einen Regex-Ersatz verwenden, um einen Globus in einen Regex zu verwandeln?
Tim Sylvester
1
Die Zeilen oben, die das führende und nachfolgende '*' entfernen, müssen für Java entfernt werden, da String.matches nur für den gesamten String gilt
KitsuneYMG
10
Zu Ihrer Information
Stephen C
59

Globbing ist auch für die Implementierung in Java 7 geplant.

Siehe FileSystem.getPathMatcher(String)und das Tutorial "Dateien suchen" .

finnw
quelle
23
Wunderbar. Aber warum um alles in der Welt ist diese Implementierung auf "Pfad" -Objekte beschränkt?!? In meinem Fall möchte ich URI entsprechen ...
Yves Martin
3
Wenn man sich die Quelle von sun.nio ansieht, scheint das Glob-Matching von Globs.java implementiert zu sein . Leider wurde dies speziell für Dateisystempfade geschrieben, sodass es nicht für alle Zeichenfolgen verwendet werden kann (es werden einige Annahmen über Pfadtrennzeichen und unzulässige Zeichen getroffen). Aber es kann ein hilfreicher Ausgangspunkt sein.
Neil Traft
32

Vielen Dank an alle hier für ihre Beiträge. Ich habe eine umfassendere Konvertierung geschrieben als jede der vorherigen Antworten:

/**
 * 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();
}

Und das Gerät testet, um zu beweisen, dass es funktioniert:

/**
 * @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
quelle
Danke für diesen Code, Neil! Wären Sie bereit, ihm eine Open Source-Lizenz zu geben?
Steven
1
Ich gebe hiermit zu, dass der Code in dieser Antwort gemeinfrei ist.
Neil Traft
Soll ich noch etwas tun? :-P
Neil Traft
9

Es gibt einige Bibliotheken, die Glob-ähnliche Mustervergleiche durchführen, die moderner sind als die aufgeführten:

Theres Ants Directory Scanner und Federn AntPathMatcher

Ich empfehle beides gegenüber den anderen Lösungen, da Ant Style Globbing in der Java-Welt (Hudson, Spring, Ant und ich denke Maven) so ziemlich zur Standard-Glob-Syntax geworden ist .

Adam Gent
quelle
1
Hier sind die Maven-Koordinaten für das Artefakt mit AntPathMatcher: search.maven.org/… und einige Tests mit Beispielverwendung: github.com/spring-projects/spring-framework/blob/master/…
seanf
Und Sie können das "Pfad" -Zeichen anpassen ... so ist es nützlich für andere Dinge als Pfade ...
Michael Wiles
7

Ich musste es vor kurzem tun und benutzte \Qund \Edem Glob-Muster entkommen:

private static Pattern getPatternFromGlob(String glob) {
  return Pattern.compile(
    "^" + Pattern.quote(glob)
            .replace("*", "\\E.*\\Q")
            .replace("?", "\\E.\\Q") 
    + "$");
}
Vincent Robert
quelle
4
Wird diese Pause nicht unterbrochen, wenn irgendwo in der Zeichenfolge ein \ E vorhanden ist?
jmo
@jmo, ja, aber Sie können dies umgehen, indem Sie die globVariable mit glob = Pattern.quote (glob) vorverarbeiten, was meiner Meinung nach solche Randfälle behandelt. In diesem Fall müssen Sie jedoch das erste und das letzte \\ Q und \\ E nicht voranstellen und anhängen.
Kimball Robinson
2
@jmo Ich habe das Beispiel für die Verwendung von Pattern.quote () korrigiert.
dimo414
5

Dies ist eine einfache Glob-Implementierung, die * und? im Muster

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
quelle
4

Ähnlich wie Tony Edgecombe ‚s Antwort , hier ist eine kurze und einfache globber , dass Träger *und ?ohne regex, wenn jemand braucht.

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
quelle
1
Ausgezeichnete Antwort tihi! Dies ist einfach genug, um es bei einer kurzen Lektüre zu verstehen und nicht zu verwirrend :-)
Begrenzte Versöhnung
3

Es kann ein leicht hackiger Ansatz sein. Ich habe es aus dem NIO2- Files.newDirectoryStream(Path dir, String glob)Code herausgefunden. Achten Sie darauf, dass jedes passende neue PathObjekt erstellt wird. Bisher konnte ich dies nur unter Windows FS testen, ich glaube jedoch, dass es auch unter Unix funktionieren sollte.

// 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 Funktioniert sowohl unter Mac als auch unter Linux.

Andrii Karaivanskyi
quelle
0

Vor langer Zeit habe ich eine massive globgesteuerte Textfilterung durchgeführt, also habe ich ein kleines Stück Code geschrieben (15 Codezeilen, keine Abhängigkeiten über JDK hinaus). Es behandelt nur '*' (war für mich ausreichend), kann aber leicht für '?' Erweitert werden. Es ist um ein Vielfaches schneller als vorkompilierter regulärer Ausdruck und erfordert keine Vorkompilierung (im Wesentlichen handelt es sich jedes Mal um einen String-gegen-String-Vergleich, wenn das Muster übereinstimmt).

Code:

  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;
    }
  }

Verwendung:

  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();
    }
  }

Von hier aus kopieren / einfügen

Bobah
quelle
Da es nur 15 Zeilen sind, sollten Sie es hier einfügen, falls die verlinkte Seite ausfällt.
Raniz
0

Die vorherige Lösung von Vincent Robert / dimo414 basiert auf Pattern.quote()der Implementierung in Bezug auf \Q... \E, was nicht in der API dokumentiert ist und daher möglicherweise nicht für andere / zukünftige Java-Implementierungen der Fall ist. Die folgende Lösung beseitigt diese Implementierungsabhängigkeit, indem alle Vorkommen von \Eanstatt von verwendet werden quote(). Es aktiviert auch DOTALLmode ( (?s)) für den Fall, dass die zuzuordnende Zeichenfolge Zeilenumbrüche enthält.

    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
quelle
-1

Übrigens scheint es, als hätten Sie es in Perl auf die harte Tour gemacht

Dies macht den Trick in Perl:

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

quelle
1
Das funktioniert nur, wenn der Glob zum Abgleichen von Dateien dient. Im Perl-Fall stammten die Globs tatsächlich aus einer Liste von IP-Adressen, die aus Gründen, auf die ich nicht eingehen möchte, mit Globs geschrieben wurden, und in meinem aktuellen Fall sollten die Globs mit URLs übereinstimmen.
Paul Tomblin