¿Existe un equivalente de java.util.regex para patrones de tipo "glob"?

84

¿Existe una biblioteca estándar (preferiblemente Apache Commons o similarmente no viral) para hacer coincidencias de tipo "glob" en Java? Cuando tuve que hacer algo similar en Perl una vez, simplemente cambié todos los " ." a " \.", " *" a " .*" y " ?" a " ." y ese tipo de cosas, pero me pregunto si alguien ha hecho el trabaja para mi.

Pregunta similar: crear expresiones regulares a partir de una expresión glob

Paul Tomblin
fuente
GlobCompiler / GlobEngine , de Yakarta ORO , parece prometedor. Está disponible bajo la licencia Apache.
Steve Trout
¿Podría dar un ejemplo preciso de lo que quiere hacer?
Thorbjørn Ravn Andersen
Lo que quiero hacer (o más bien lo que mi cliente quiere hacer) es hacer coincidir cosas como " -2009 /" o "* rss " en las URL. En su mayoría, es bastante trivial convertir a expresiones regulares, pero me preguntaba si habría una manera más fácil.
Paul Tomblin
Recomiendo el globing de archivos de estilo Ant, ya que parece haberse convertido en el globing canónico en el mundo de Java. Consulte mi respuesta para obtener más detalles: stackoverflow.com/questions/1247772/… .
Adam Gent
1
@BradMace, relacionado, pero la mayoría de las respuestas asumen que está atravesando un árbol de directorios. Aún así, si alguien todavía está buscando cómo hacer una coincidencia de estilo global de cadenas arbitrarias, probablemente también debería buscar en esa respuesta.
Paul Tomblin

Respuestas:

46

No hay nada incorporado, pero es bastante simple convertir algo parecido a un globo en una expresión regular:

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

esto funciona para mí, pero no estoy seguro de si cubre el "estándar" global, si hay uno :)

Actualización de Paul Tomblin: encontré un programa perl que hace conversión global y, al adaptarlo a Java, termino con:

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

Estoy editando esta respuesta en lugar de hacer la mía propia porque esta respuesta me puso en el camino correcto.

Dave Ray
fuente
1
Sí, esa es prácticamente la solución que se me ocurrió la última vez que tuve que hacer esto (en Perl), pero me preguntaba si había algo más elegante. Creo que lo haré a tu manera.
Paul Tomblin
1
En realidad, encontré una mejor implementación en Perl que puedo adaptar a Java en kobesearch.cpan.org/htdocs/Text-Glob/Text/Glob.pm.html
Paul Tomblin
¿No podría usar un reemplazo de expresiones regulares para convertir un globo en una expresión regular?
Tim Sylvester
1
Las líneas en la parte superior que eliminan el '*' inicial y final deben eliminarse para java ya que String.matches solo contra toda la cadena
KitsuneYMG
10
FYI: El estándar para 'globbing' es el lenguaje Shell POSIX - opengroup.org/onlinepubs/009695399/utilities/…
Stephen C
60

Globbing también está planeado para implementarse en Java 7.

Ver FileSystem.getPathMatcher(String)y el tutorial "Búsqueda de archivos" .

finnw
fuente
23
Maravilloso. Pero, ¿por qué diablos esta implementación se limita a los objetos "Path"? En mi caso, quiero hacer coincidir URI ...
Yves Martin
3
Mirando a la fuente de sun.nio, la coincidencia global parece ser implementada por Globs.java . Desafortunadamente, esto está escrito específicamente para las rutas del sistema de archivos, por lo que no se puede usar para todas las cadenas (hace algunas suposiciones sobre los separadores de ruta y los caracteres ilegales). Pero puede ser un punto de partida útil.
Neil Traft
33

Gracias a todos los aquí presentes por sus contribuciones. Escribí una conversión más completa que cualquiera de las respuestas anteriores:

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

Y las pruebas unitarias para demostrar que funciona:

/**
 * @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
fuente
¡Gracias por este código, Neil! ¿Estarías dispuesto a darle una licencia de código abierto?
Steven
1
Por la presente concedo que el código de esta respuesta es de dominio público.
Neil Traft
¿Debo hacer algo más? :-P
Neil Traft
9

Hay un par de bibliotecas que hacen coincidir patrones tipo Glob que son más modernas que las enumeradas:

Theres Ants Directory Scanner y Springs AntPathMatcher

Recomiendo ambas sobre las otras soluciones, ya que Ant Style Globbing se ha convertido prácticamente en la sintaxis glob estándar en el mundo Java (Hudson, Spring, Ant y creo que Maven).

Adam Gent
fuente
1
Aquí están las coordenadas de Maven para el artefacto con AntPathMatcher: search.maven.org/… Y algunas pruebas con uso de muestra: github.com/spring-projects/spring-framework/blob/master/…
seanf
Y se puede personalizar el carácter "camino" ... por lo que es útil para otras cosas aparte de caminos ...
Michael Wiles
7

Recientemente tuve que hacerlo y usé \Qy \Epara escapar del patrón glob:

private static Pattern getPatternFromGlob(String glob) {
  return Pattern.compile(
    "^" + Pattern.quote(glob)
            .replace("*", "\\E.*\\Q")
            .replace("?", "\\E.\\Q") 
    + "$");
}
Vincent Robert
fuente
4
¿No se romperá esto si hay una \ E en algún lugar de la cadena?
jmo
@jmo, sí, pero puede evitarlo procesando previamente la globvariable con glob = Pattern.quote (glob), que creo que maneja tales casos extremos. En ese caso, sin embargo, no es necesario anteponer y agregar la primera y la última \\ Q y \\ E.
Kimball Robinson
2
@jmo He arreglado el ejemplo para usar Pattern.quote ().
dimo414
5

Esta es una implementación Glob simple que maneja * y? en el patrón

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
fuente
5

Similar a la respuesta de Tony Edgecombe , aquí hay un globber corto y simple que admite y sin usar expresiones regulares, si alguien lo necesita.*?

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
fuente
1
Excelente respuesta tihi! Esto es lo suficientemente simple para entender en una lectura rápida y no demasiado desconcertante :-)
Expiación limitada
3

Puede ser un enfoque un poco hacker. Lo descubrí a partir del Files.newDirectoryStream(Path dir, String glob)código de NIO2 . Preste atención a que Pathse crea cada nuevo objeto coincidente . Hasta ahora pude probar esto solo en Windows FS, sin embargo, creo que también debería funcionar en Unix.

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

ACTUALIZAR Funciona tanto en Mac como en Linux.

Andrii Karaivanskyi
fuente
0

Hace mucho tiempo, estaba haciendo un filtrado de texto masivo basado en glob, así que escribí un pequeño fragmento de código (15 líneas de código, sin dependencias más allá de JDK). Maneja solo '*' (fue suficiente para mí), pero se puede extender fácilmente para '?'. Es varias veces más rápido que las expresiones regulares precompiladas, no requiere ninguna compilación previa (esencialmente, es una comparación de cadena contra cadena cada vez que el patrón coincide).

Código:

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

Uso:

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

Copiar / pegar desde aquí

bobah
fuente
Dado que solo tiene 15 líneas, debe incluirlo aquí en caso de que la página vinculada se caiga.
Raniz
0

La solución anterior de Vincent Robert / dimo414 se basa en Pattern.quote()implementarse en términos de \Q... \E, que no está documentado en la API y, por lo tanto, puede no ser el caso para otras implementaciones de Java / futuras. La siguiente solución elimina esa dependencia de implementación al escapar de todas las apariciones de en \Elugar de usar quote(). También activa DOTALLmode ( (?s)) en caso de que la cadena que se va a comparar contenga nuevas líneas.

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

Por cierto, parece que lo hiciste de la manera difícil en Perl.

Esto hace el truco en Perl:

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

fuente
1
Eso solo funciona si el glob es para archivos coincidentes. En el caso de Perl, los globs en realidad provienen de una lista de direcciones IP que se escribieron usando globs por razones en las que no entraré, y en mi caso actual, los globs coincidían con las URL.
Paul Tomblin