Existe um equivalente de java.util.regex para padrões de tipo “glob”?

84

Existe uma biblioteca padrão (de preferência Apache Commons ou similarmente não viral) para fazer correspondências de tipo "glob" em Java? Quando eu tive que fazer algo semelhante em Perl uma vez, eu apenas mudei todo o " ." para " \.", o " *" para " .*" e o " ?" para " ." e esse tipo de coisa, mas estou me perguntando se alguém fez o trabalhe para mim.

Pergunta semelhante: Criar regex a partir da expressão glob

Paul Tomblin
fonte
GlobCompiler / GlobEngine , de Jakarta ORO , parece promissor. Ele está disponível sob a licença Apache.
Steve Trout
Você poderia dar um exemplo preciso do que você deseja fazer?
Thorbjørn Ravn Andersen
O que eu quero fazer (ou melhor, o que meu cliente deseja fazer) é combinar coisas como " -2009 /" ou "* rss " em urls. Na maioria das vezes, é bastante trivial converter para regex, mas eu me perguntei se havia uma maneira mais fácil.
Paul Tomblin
Eu recomendo o globing de arquivo do estilo Ant, pois parece ter se tornado o globing canônico no mundo Java. Veja minha resposta para mais detalhes: stackoverflow.com/questions/1247772/… .
Adam Gent
1
@BradMace, relacionado, mas a maioria das respostas presume que você está percorrendo uma árvore de diretório. Ainda assim, se alguém ainda está procurando como fazer a correspondência no estilo glob de strings arbitrárias, provavelmente também deve procurar nessa resposta.
Paul Tomblin

Respostas:

46

Não há nada integrado, mas é muito simples converter algo semelhante a glob em um regex:

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

isso funciona para mim, mas não tenho certeza se cobre o "padrão" global, se houver :)

Atualização de Paul Tomblin: Encontrei um programa perl que faz conversão glob e, adaptando-o para Java, acabei com:

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

Estou editando esta resposta em vez de fazer a minha, porque essa resposta me colocou no caminho certo.

Dave Ray
fonte
1
Sim, essa é a solução que eu encontrei da última vez que tive que fazer isso (em Perl), mas estava me perguntando se havia algo mais elegante. Acho que vou fazer do seu jeito.
Paul Tomblin
1
Na verdade, encontrei uma implementação melhor em Perl que posso adaptar para Java em kobesearch.cpan.org/htdocs/Text-Glob/Text/Glob.pm.html
Paul Tomblin
Você não poderia usar uma substituição de regex para transformar um glob em um regex?
Tim Sylvester
1
As linhas na parte superior que retiram o '*' inicial e final precisam ser removidas para java, pois String.matches contra a string inteira apenas
KitsuneYMG
10
Para sua informação: O padrão para 'globbing' é a linguagem POSIX Shell - opengroup.org/onlinepubs/009695399/utilities/…
Stephen C
60

Globbing também está planejado para implementação em Java 7.

Veja FileSystem.getPathMatcher(String)e o tutorial "Localizando Arquivos" .

Finnw
fonte
23
Maravilhoso. Mas por que diabos essa implementação é limitada a objetos "Path"?!? No meu caso, quero corresponder URI ...
Yves Martin
3
Analisando a fonte de sun.nio, a correspondência glob parece ser implementada por Globs.java . Infelizmente, isso é escrito especificamente para caminhos de sistema de arquivos, portanto, não pode ser usado para todas as strings (faz algumas suposições sobre separadores de caminho e caracteres ilegais). Mas pode ser um ponto de partida útil.
Neil Traft
33

Obrigado a todos aqui por suas contribuições. Escrevi uma conversão mais abrangente do que qualquer uma das respostas 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();
}

E os testes de unidade para provar 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
fonte
Obrigado por este código, Neil! Você estaria disposto a dar a ele uma licença de código aberto?
Steven
1
Eu admito que o código nesta resposta é de domínio público.
Neil Traft,
Devo fazer mais alguma coisa? :-P
Neil Traft
9

Existem algumas bibliotecas que fazem correspondência de padrões do tipo Glob que são mais modernas do que as listadas:

Theres Ants Directory Scanner e Springs AntPathMatcher

Eu recomendo ambas as outras soluções, já que Ant Style Globbing praticamente se tornou a sintaxe glob padrão no mundo Java (Hudson, Spring, Ant e eu acho que Maven).

Adam Gent
fonte
1
Aqui estão as coordenadas Maven para o artefato com AntPathMatcher: search.maven.org/… E alguns testes com uso de amostra: github.com/spring-projects/spring-framework/blob/master/…
seanf
E você pode personalizar o caractere "caminho" ... então é útil para outras coisas além de caminhos ...
Michael Wiles
7

Recentemente, tive que fazer isso e usei \Qe \Epara escapar do padrão glob:

private static Pattern getPatternFromGlob(String glob) {
  return Pattern.compile(
    "^" + Pattern.quote(glob)
            .replace("*", "\\E.*\\Q")
            .replace("?", "\\E.\\Q") 
    + "$");
}
Vincent Robert
fonte
4
Isso não vai quebrar se houver um \ E em algum lugar da string?
jmo
@jmo, sim, mas você pode contornar isso pré-processando a globvariável com glob = Pattern.quote (glob), que acredito lidar com esses casos extremos. Nesse caso, porém, você não precisa preceder e acrescentar o primeiro e o último \\ Q e \\ E.
Kimball Robinson de
2
@jmo Corrigi o exemplo para usar Pattern.quote ().
dimo414
5

Esta é uma implementação simples de Glob que lida com * e? no padrão

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

Semelhante ao Tony Edgecombe 's resposta , aqui está uma pequena e simples Globber que os apoios *e ?sem usar regex, se alguém precisa de um.

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
fonte
1
Excelente resposta, tihi! Isso é simples o suficiente de entender em uma leitura rápida e não muito confuso :-)
Expiação limitada de
3

Pode ser uma abordagem ligeiramente hacky. Eu descobri no Files.newDirectoryStream(Path dir, String glob)código do NIO2 . Preste atenção que a cada novo Pathobjeto correspondente é criado. Até agora eu fui capaz de testar isso apenas no Windows FS, no entanto, acredito que deve funcionar no Unix também.

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

ATUALIZAÇÃO Funciona em Mac e Linux.

Andrii Karaivanskyi
fonte
0

Há muito tempo, eu estava fazendo uma enorme filtragem de texto baseada em glob, então escrevi um pequeno trecho de código (15 linhas de código, sem dependências além do JDK). Ele lida apenas com '*' (era suficiente para mim), mas pode ser facilmente estendido para '?' É várias vezes mais rápido do que o regexp pré-compilado, não requer nenhuma pré-compilação (essencialmente, é uma comparação string-vs-string sempre que o padrão é correspondido).

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

Copie / cole daqui

bobah
fonte
Como são apenas 15 linhas, você deve incluí-lo aqui, caso a página vinculada caia.
Raniz
0

A solução anterior de Vincent Robert / dimo414 depende de Pattern.quote()ser implementada em termos de \Q... \E, o que não está documentado na API e, portanto, pode não ser o caso para outras / futuras implementações Java. A solução a seguir remove essa dependência de implementação ao escapar de todas as ocorrências de em \Evez de usar quote(). Ele também ativa DOTALLmode ( (?s)) no caso de a string a ser correspondida conter novas linhas.

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

A propósito, parece que você fez da maneira mais difícil em Perl

Isso funciona em Perl:

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

fonte
1
Isso só funciona se o glob for para arquivos correspondentes. No caso do perl, os globs, na verdade, vieram de uma lista de endereços IP que foi escrita usando globs por motivos que não vou entrar em detalhes e, no meu caso atual, os globs deviam corresponder aos urls.
Paul Tomblin,