Usando POSIX.2 regular expressions em C

A desculpa para aprender PERL ou Python, às vezes, é a facilidade com que pode-se usar regular expressions (que vou chamar de regex daqui pra frente) nessas linguagens. Ou, melhor ainda, a impossibilidade de usá-las em C. Isso não é verdade se você está acostumado, como eu, a usar o GNU C/C++ Compiler. A biblioteca padrão de C (libc) possui 3 funções específicas para lidar com regex, no sabor POSIX.2.

Não vou explicar como construir uma regex. Basta dizer que usá-las significa poupar um bocado de código com comparação de padrões de strings. Uma regex que pode ser usada para verificar o padrão de um endereço de e-mail seria mais ou menos parecida com isso: “^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$”. No seu programa, ao testar uma string contra uma regex, obterá um “match” (traduzível como “string bate com o padrão da regex) ou “not match”.

Tudo o que você precisa fazer para usar as funções de regex é incorporar o header regex.h e as 3 funções declaradas nele:

int regcomp(regex_t *preg, const char *regex, int cflags);
int regexec(const regex_t *preg, const char *string, 
            size_t match, regmatch_t pmatch[], int eflags);
void regfree(regex_t *preg);

A função regcomp() retorna 0 se a regex estiver correta, a função regexc retorna 0 se houve um match da string contra a regex.

A primeira coisa a fazer é compilar a regex chamando a função regcomp(). Se ela falhar, significa que tem um erro na sua regex:

#define MY_REGEX "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}$"

regex_t re;

if (regcomp(&re, MY_REGEX, REG_EXTENDED))
{
  /* Tratamento de erro aqui. */
}

Uma vez que a regex esteja compilada, basta usar regexec() para verificar se houve um match:

if (regexec(&re, str, 0, NULL, 0) == 0)
{
  /* Se a string 'str' *bate* com o padrão da regex, faz algo aqui! */
}

E, finalmente, temos que nos livrar da regex compilada:

regfree(&re);

Simples e fácil. Mas, porque regexec() tem esses 3 parâmetros no final da função? O último parâmtro são flags que podem ser usados para mudar o significado de ‘^’ e ‘$’ na regex. Os parâmetros matchpmatch são, respectivamente, um contador de matches e um array de strings matched. Isso está ai porque numa regex podemos ter múltiplos agrupamentos, que podem ser obtidos como matches individuais. Por exemplo, suponha que na regex que testa e-mails, acima, queiramos saber, separadamente, o nome do usuário e o nome do servidor. Poderíamos criar agrupamentos assim: “^([A-Za-z0-9._%+-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,4})$”. Notou os parenteses?

A função regexec(), se receber um valor diferente de 0 no parâmetro match e um ponteiro diferente de NULL em pmatch, preencherá o array pmatch com o par de índices − início e fim − para string original onde estão as substrings dos agrupamentos. O primeiro item do array conterá os índices que batem com a regex inteira. O segundo item, para o primeiro agrupamento; O terceiro, para o segundo e assim por diante. O tipo regmatch_t contém apenas dois inteiros: rm_so e rm_eo (abreviações de “Regex Match Start Offset” e “Regex Match End Offset“. A especificação da função regexec() nos diz que, se o membro rm_so for -1, então não há um “match” na posição específica do array pmatch.

No código acima, se quisermos obter esses offsets dos matches da regex do e-mail, com os agrupamentos, teríamos que declarar o array e chamar regexec() assim:

/* Declara array com 3 itens: O match inteiro e os 2 agrupamentos. */
regmatch_t rm[3];

if (regexec(&re, str, sizeof(rm), rm, 0) == 0)
{
  /* Se a string 'str' *bate* com o padrão da regex, faz algo aqui! */
}

Cabe a você obter as substrings a partir desses offsets e da string original.

Um exemplo complexo (mas, nem tanto).

Eis como obter um endereço IP (que pode ser parcial) e pode ter um CIDR atachado a ele, usando regex:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <regex.h>

/* Macro usada para testar se há um "match" no array 
   preenchido por regexec(). */
#define MATCH(x) ((x).rm_so >= 0)

/* Conserta "bug" de strncpy().
   Parece que strncpy() não coloca '' no fim da 
   string copiada. */
#define COPY_SUBSTR(d, s, l) { \
    strncpy((d), (s), (l)); \
    *((d) + (l)) = ''; \
  }

typedef struct
{
  unsigned addr;
  unsigned cidr;
} CIDR;

/* Regex, no formato POSIX.2 ERE (Extended).
   Complicado, né? Com múltiplos agrupamentos! */
#define IP_REGEX "^([1-2]*[0-9]{1,2})" \
  "(\\.[1-2]*[0-9]{1,2}){0,1}" \
  "(\\.[1-2]*[0-9]{1,2}){0,1}" \
  "(\\.[1-2]*[0-9]{1,2}){0,1}" \
  "(/[0-9]{1,2}){0,1}$"

/* Obtem IP e CIDR a partir de uma string.
   Retorna 0, em caso de falha e 1, se tudo ok. */
int str_to_ip_and_cidr(char const *const addr, CIDR *cidr_ptr)
{
  regex_t re;
  regmatch_t rm[6];
  unsigned matches[5];
  int i, len;
  char *t;

  cidr_ptr->addr = cidr_ptr->cidr = 0;

  /* Tenta compilar a regex. */
  if (regcomp(&re, IP_REGEX, REG_EXTENDED))
    return 0;

  /* Executa a regex contra a string. */
  if (regexec(&re, addr, sizeof(rm), rm, 0))
  {
    regfree(&re);
    return 0;
  }

  /* Aloca espaço suficiente para as substrings. */
  if ((t = strdup(addr)) == NULL)
  {
    perror("Cannot allocate temporary string");
    abort();
  }

  /* Converte os octetos que "bateram".
     Pela regex, o primeiro sempre "bate".
   */
  len = rm[1].rm_eo - rm[1].rm_so;
  COPY_SUBSTR(t, addr + rm[1].rm_so, len);
  matches[0] = atoi(t);

  /* Converte os outros 3 octetos, se existirem.
     Note que as substrings começam com '.'. */
  for (i = 2; i <= 4; i++)
  {
    /* Pela regex, esses octetos podem nem
       ter sido informados! */
    if (MATCH(rm[i]))
    {
      len = rm[i].rm_eo - rm[i].rm_so - 1;
      COPY_SUBSTR(t, addr + rm[i].rm_so + 1, len);
      matches[i - 1] = atoi(t);
    }
    else
      matches[i - 1] = 0;
  }

  /* Converte o cidr.
     Pela regex ele pode nem ter sido informado!
     Note que a substring começa com '/'. */

  if (MATCH(rm[5]))
  {
    len = rm[5].rm_eo - rm[5].rm_so - 1;
    COPY_SUBSTR(t, addr + rm[5].rm_so + 1, len);

    /* CIDRs merecem um tratamento especial.
       Quando informados, precism ser >= 1. */
    if ((matches[4] = atoi(t)) == 0)
    {
      /* if cidr is actually '0', then it is an error! */
      free(t);
      regfree(&re);
      return 0;
    }
  }
  else
    matches[4] = 32;

  /* Não precisamos mais da string temporária. */
  free(t);

  /* Valida os octetos */
  for (i = 0; i < 4; i++)
    if (matches[i] > 255)
    {
      regfree(&re);
      return 0;
    }

  /* Valida o cidr. */
  if (matches[4] > 32)
  {
    regfree(&re);
    return 0;
  }

  /* Prepara a estrutura do CIDR. */
  cidr_ptr->cidr = matches[4];
  cidr_ptr->addr = (((matches[3] & 0xff))       |
                    ((matches[2] & 0xff) << 8)  |
                    ((matches[1] & 0xff) << 16) |
                    ((matches[0] & 0xff) << 24)) &
                   (0xffffffffUL << (32 - cidr_ptr->cidr));

  regfree(&re);
  return 1;
}

A função acima aceita as variações como verdadeiras: a; a.b; a.b.c; a.b.c.d; a/n; a.b/n; a.b.c/n e a.b.c.d/n − onde a, b, c e d podem ser valores entre 0 e 255 e n entre 1 e 32. Em qualquer outro caso a função retorna 0 (falha). A função ainda toma conta de outro detalhe: Se fornecermos a string “10.4.23.2/16”, o valor que estará no campo addr da estrutura CIDR será equivalente a 10.4.0.0/16, já que 16 bits de CIDR significa que os 16 bit superiores é que contam (netmask de 16 bits).

Uma “feature” adicional de minha rotina: Se ela falhar, a estrutura CIDR conterá { 0, 0 }!

A função parece grande, mas garanto que fazer uma lógica parecida usando strtok(), por exemplo, a faria bem mais complicada!

Uma nota de cautela

Existem vários “sabores” de regexes por ai. PERL tem o dele próprio, por exemplo… As regexes válidas na libc são aquelas que seguem o padrão POSIX.2 (tanto a básica quanto a extendida). É útil dar uma olhada na manpage sobre regex:

$ man 7 regex

Outro cuidado é quanto ao compilador… Se você usa o Visual C++ não encontrará essa implementação por lá. Ao que parece, a implementação da Microsoft só existe em C++, em forma de classe, e apenas para a variação da gramática de C++ para .NET. Então, se você quer usar regexes nos seus programas em C, no Windows, recomendo o uso do MinGW32 ou MinGW64. Isso tem uma vantagem: Ambos estão disponíveis, em forma de pacotes, inclusive para Linux (sim! vocẽ pode desenvolver aplicações para Windows no Linux!).

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s