Usando Expressões Regulares na Linguagem C

Thobias Salazar Trevisan
06/07/2004



Introdução

Expressão Regular é um método que utiliza alguns caracteres com significado especial para especificar um padrão de texto. O suporte a expressões regulares (também conhecida como regexp, regex e ER) pode ser encontrado em programas como sed, awk e grep, em editores de texto como vi e emacs, e em linguagens de programação como C, perl, java, python, php etc. Como se pode ver, um dia as expressões regulares vão dominar o mundo! 8=)

Se você não entendeu ou não sabe o que são, pra que servem ou como usar? acesse http://aurelio.net/er e divirta-se!!

A utilização de ER pode facilitar muito a programação de parsers, validação de dados, busca de textos... Mas não vou ficar falando dos benefícios de usá-las ou como elas podem aumentar sua produtividade, economizar muito o seu tempo, transformar tarefas chatas em emocionantes etc. O objetivo deste texto é mostrar como utilizar ER na linguagem C.

Expressões regulares têm três interfaces para C: a projetada pelo GNU, a compatível com BSD e a compatível com POSIX. A última será a abordada neste texto.

Pré-requisito: Conhecimentos básicos de expressões regulares e da linguagem de programação C.

Como de costume, este texto é extremamente prático. Aperte o sinto, segure-se na poltrona e vamos começar. =8)

Início

Existem quatro funções para ER POSIX em C: regcomp, regexec, regerror e regfree. O cabeçalho destas funções está no arquivo regex.h. Este arquivo também tem os defines para duas estruturas: regex_t e regmatch_t.

A utilização de ER em C se dá através de dois passos: Primeiro deve-se compilar/converter a ER (string) em um pattern buffer, que em POSIX, é do tipo regex_t. Após este passo, pode-se casar a ER compilada com o input.

Para compilar uma dada ER em um pattern buffer utiliza-se a função regcomp:

  int regcomp(regex_t *preg, const char *regex, int cflags);

'preg' é um ponteiro para um pattern buffer (regex_t). 'regex' é um ponteiro para uma string que contém a expressão regular. 'cflags' é utilizada para determinar o tipo de compilação. As 'cflags' são:

REG_EXTENDED para usar a sintaxe de POSIX Extended Regular Expression, caso contrário é utilizado POSIX Basic Regular Expression.
REG_ICASE para ignorar maiúsculas e minúsculas (ignore case).
REG_NOSUB os parâmetros nmatch e pmatch da função regexec são ignorados. Utilizado somente para saber se a ER casa ou não.
REG_NEWLINE mesmo que o input tenha várias linhas, serão tratadas como se fossem independentes. ex: '^1' e '^1$' com uma entrada '1\n1' casariam duas vezes. '1\n1' casaria uma.

Após compilar a ER pode-se tentar casá-la com uma dada entrada (input) através da função regexec:

  int regexec(const  regex_t  *preg,  const  char *string, size_t nmatch,
                     regmatch_t pmatch[], int eflags);

'preg' é um ponteiro para o pattern buffer (regex_t). 'nmatch' e 'pmatch' têm informações sobre a localização dos matches na entrada (input). 'eflags' é usado para alterar o comportamento do match:

REG_NOTBOL o metacaractere '^' quando usado para marcar início de linha não tem efeito, ou seja, início da string passada (char *string) não deve ser considerado início de linha.
REG_NOTEOL o mesmo que REG_NOTBOL, mas para o caractere '$' que marca final de linha.

Você entenderá o motivo destas flags no decorrer do tutorial. Não se preocupe com elas agora.

Casa ou Não Casa

Esta pergunta depende muito do contexto. :)

Chega de teoria, hora da prática para clarear as idéias. O exemplo mais simples é saber se uma expressão regular casa ou não com uma determinada entrada.

/*
 * match.c
 *
 * Este programa simplesmente testa se uma expressão regular casa (match)
 * com uma entrada (input).
 *
 * argv[1] = expressão regular
 * argv[2] = input
 *
 * ex: ./match '^12' '1234567890'
 */

/* headers necessários */
#include <stdio.h>
#include <stdlib.h>
#include <regex.h>

/* recebe como parâmetro a expressão regular e o input para
 * tentar casar */
void er_match(char *argv[])
{
	/* aloca espaço para a estrutura do tipo regex_t */
	regex_t reg;

	/* compila a ER passada em argv[1]
	 * em caso de erro, a função retorna diferente de zero */
	if (regcomp(&reg , argv[1], REG_EXTENDED|REG_NOSUB) != 0) {
		fprintf(stderr,"erro regcomp\n");
		exit(1);
	}
	/* tenta casar a ER compilada (&reg) com a entrada (argv[2]) 
	 * se a função regexec retornar 0 casou, caso contrário não */
	if ((regexec(&reg, argv[2], 0, (regmatch_t *)NULL, 0)) == 0)
		printf("Casou\n");
	else
		printf("Não Casou\n");
}

int main(int argc, char **argv)
{
	if (argc != 3) {
		fprintf(stderr,"Uso: match <ER> <input>\n");
		return 1;
	}
	er_match(argv);
	return 0;
}
Executando:

  prompt> ./match '12' '1234567890'
  Casou
  
  prompt> ./match '^12' '1234567890'
  Casou
  
  prompt> ./match '^ 12' '1234567890'
  Não Casou
  
  prompt> ./match '[a-z]' '1234567890'
  Não Casou

String de Erro

Executando o programa anterior com uma ER inválida tem-se a seguinte saída:

  prompt> ./match '[a-z' '1234567890'
  erro regcomp

Pode-se utilizar a função regerror para transformar o código de erro retornado por regcomp e regexec em uma mensagem de erro e, assim, dando uma dica do problema na ER.

   size_t regerror(int errcode, const regex_t *preg, char *errbuf,  size_t
                         errbuf_size);

'errcode' é o erro retornado por regcomp ou regexec. 'preg' é um ponteiro para o pattern buffer . 'errbuf' um buffer que conterá a mensagem de erro. 'errbuf_size' é o tamanho da string de erro.

Se a função regerror for chamada com errbuf como NULL e errbuf_size como zero, ela retorna o tamanho da mensagem de erro.

/*
 * er_error.c
 *
 * Testa se uma expressão regular casa (match)
 * com uma linha e faz tratamento de erro para ER inválidas...
 */

/* headers necessários */
#include <stdio.h>
#include <stdlib.h>
#include <regex.h>

/* mostra uma mensagem do erro usando regerror */
int er_error(int i, regex_t reg)
{
	size_t length;
	char *buffer=NULL;

	/* pega o tamanho da mensagen de erro */
	length = regerror (i, &reg, NULL, 0);

	/* aloca espaço para a mensagem de erro */
	if ((buffer = (char *)malloc (length)) == NULL) {
		fprintf(stderr, "error: malloc buffer\n");
		exit(1);
	}
	
	/* coloca em buffer a mensagem de erro */
	regerror (i, &reg, buffer, length);
	
	fprintf(stderr,"%s\n",buffer);
	free(buffer);
	exit(1);
}

/* tenta casar uma ER com o input */
void er_match(char *argv[])
{
	/* aloca espaço para a estrutura do tipo regex_t */
	regex_t reg;
	int i;

	/* compila a ER passada em argv[1]
	 * em caso de erro, a função retorna diferente de zero */
	if ((i=regcomp(&reg , argv[1], REG_EXTENDED|REG_NOSUB)) != 0)
		/* imprime a string do erro */
		er_error(i,reg);	

	/* tenta casar a ER compilada (&reg) com a entrada (argv[2]) 
	 * se a função regexec retornar 0 casou, caso contrário não */
	if ((regexec(&reg, argv[2], 0, (regmatch_t *)NULL, 0)) == 0)
		printf("Casou\n");
	else
		printf("Não Casou\n");
}

int main(int argc, char **argv)
{
	if (argc != 3) {
		fprintf(stderr,"Uso: er_error <ER> <input>\n");
		return 1;
	}
	er_match(argv);
	return 0;
}

Analisando algumas mensgens de erro:

  prompt> ./er_error '[a-z' '1234567890'
  Invalid regular expression
  
  prompt> ./er_error '(12\1' '1234567890'
  Invalid back reference
  
  prompt> ./er_error '[a-#]' '1234567890'
  Invalid range end

my_grep

Para finalizar esta seção, um programa que faz um grep em um arquivo, ou seja, mostra somente as linhas que casam com a expressão regular passada na linha de comando.

/*
 * my_grep.c
 *
 * lê um arquivo e imprime somente as linhas que casarem com a 
 * expressão regular passada
 *
 * argv[1] = expressão regular
 * argv[2] = arquivo
 *
 * ex: ./my_grep '^er' arquivo.txt
 */

/* headers necessários */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>

/* mostra uma mensagem do erro usando regerror */
int er_error(int i, regex_t reg)
{
	size_t length;
	char *buffer=NULL;

	/* pega o tamanho da mensagen de erro */
	length = regerror (i, &reg, NULL, 0);

	/* aloca espaço para a mensagem de erro */
	if ((buffer = (char *)malloc (length)) == NULL) {
		fprintf(stderr, "error: malloc buffer\n");
		exit(1);
	}
	
	/* coloca em buffer a mensagem de erro */
	regerror (i, &reg, buffer, length);
	
	fprintf(stderr,"Erro: %s\n",buffer);
	free(buffer);
	exit(1);
}

/* lê uma linha inteira do arquivo */
int get_line(char **line, FILE *fp)
{
	int tam=2;
	int start=0;

	if ((*line = (char *) malloc(tam)) == NULL) {
		fprintf(stderr, "Erro: malloc line\n");
		exit(1);
	}
	while (fgets(*line+start, tam, fp) != NULL) {
		if (strchr(*line, &#9;\n&#9;) != NULL)
			return 1;
		start += tam-1;
		*line = (char *)realloc(*line, start+1+tam);
	}
	return -1;
}

/* tenta casar uma ER com o input */
void er_match(char *er, FILE *fp)
{
	int i;
	char *line = NULL;
	/* aloca espaço para a estrutura do tipo regex_t */
	regex_t reg;

	/* compila a ER passada em argv[1]
	 * em caso de erro, a função retorna diferente de zero */
	if ((i=regcomp(&reg , er, REG_EXTENDED|REG_NEWLINE|REG_NOSUB)) != 0)
		/* imprime uma string do erro */
		er_error(i,reg);	

	/* lê o arquivo linha por linha */
	/* while ((i = getline(&line, &len, fp)) != -1) { */
	 while ((i = get_line(&line, fp)) != -1) { 
		/* teste se a linha lida casa com a ER */
		if ((regexec(&reg, line, 0, (regmatch_t *)NULL, 0)) == 0)
			printf("%s",line);
		free(line);
	}
}

int main(int argc, char **argv)
{
	FILE *fp;

	if (argc != 3) {
		fprintf(stderr,"Uso: my_grep <ER> <arquivo>\n");
		exit(1);
	}
	if ((fp = fopen(argv[2], "r")) == NULL) {
		fprintf(stderr,"Erro ao abrir %s\n",argv[2]);
		exit(1);
	}
	er_match(argv[1], fp);
	fclose(fp);
	exit(0);
}

Onde Casa

Até agora não foi utilizada a estrutura regmatch_t porque queria-se saber somente se a ER casava ou não com a entrada. Para saber qual parte da string de entrada a ER casou, deve-se utilizar a estrutura regmatch_t que contém pelo menos os seguintes campos:

regoff_t rm_so o deslcocamento (número de bytes) do início da string até o início do match, ou seja, primeiro caractere que a ER casou.
regoff_t rm_eo o deslocamento (número de bytes) do início da string até o caractere depois do match, ou seja, o caractere após o último que a ER casou.

Estas informações dizem respeito a um match. A função regexec não realiza todos os matches possíveis da linha. Ela vai lendo a entrada da esquerda para direita e após o primeiro match a função retorna.

Exemplo: imagine a seguinte ER: '12'. Com a seguinte entrada: '12012'. Dada a premissa acima, a função regexec retornará com sucesso e terá na estrutura regmatch_t o deslocamento do primeiro match, ou seja, '12012'. Se chamarmos novamente regexec com a mesma entrada, ele retornará as mesmas informações. Para tentar casar novamente a ER com a linha, deve-se passar como entrada '012', assim eliminando do início da string até o último caractere casado (rm_eo).

Outro detalhe: Com a seguinte ER: '^12'. Com uma entrada: '1212'. Teria-se 2 matches. Como ?! Após o primeiro match teria-se que chamar a regexec com uma parte da string original, ou seja, os últimos dois caracteres '12', que casaria novamente. Para resolver este problema, tem-se a flag REG_NOTBOL que 'avisa' a regexec que o operador '^', que marca início de linha, sempre falhará.

Com a ajuda do exemplo abaixo espera-se sedimentar estas regras:

/*
 * match2.c
 *
 * mostra quantas vezes a ER casou, quais partes da string
 * de entrada ela acasou...
 * 
 * argv[1] = expressão regular
 * argv[2] = entrada 
 *
 * ex: ./match2 '^er' 'string de entrada' 
 */

/* headers necessários */
#include <stdio.h>
#include <stdlib.h>
#include <regex.h>

/* mostra uma mensagem do erro usando regerror */
int er_error(int i, regex_t reg)
{
	size_t length;
	char *buffer=NULL;

	/* pega o tamanho da mensagen de erro */
	length = regerror (i, &reg, NULL, 0);

	/* aloca espaço para a mensagem de erro */
	if ((buffer = (char *)malloc (length)) == NULL) {
		fprintf(stderr, "error: malloc buffer\n");
		exit(1);
	}
	
	/* coloca em buffer a mensagem de erro */
	regerror (i, &reg, buffer, length);
	
	fprintf(stderr,"Erro: %s\n",buffer);
	free(buffer);
	exit(1);
}


/* tenta casar uma ER com o input */
void er_match(char *argv[])
{
	int i, start;
	int error;
	/* aloca espaço para a estrutura do tipo regmatch_t */
	regmatch_t match;
	/* aloca espaço para a estrutura do tipo regex_t */
	regex_t reg;

	/* compila a ER passada em argv[1]
	 * em caso de erro, a função retorna diferente de zero */
	if ((i=regcomp(&reg , argv[1], REG_EXTENDED)) != 0)
		/* imprime uma string do erro */
		er_error(i,reg);	

	printf("********** string original **********\n%s\n\n",argv[2]);
	i = start = 0;
	/* casa a ER com o input argv[2] 
	 * ^ marca início de linha */
	error = regexec(&reg, argv[2], 1, &match, 0);
	/* tenta casar a ER mais vezes na string */
	while(error == 0) {
		printf("início da string de pesquisa atual no caractere %d\n",start);
		printf("string de pesquisa = \"%s\"\n",argv[2]+start);
		printf("casou do caractere = %d ao %d\n\n",match.rm_so,match.rm_eo);
		start +=match.rm_eo; /* atualize início de string */
		i++;
		/* casa a ER com o input argv[2].  ^ não casa mais início de linha */
		error = regexec(&reg, argv[2]+start, 1, &match, REG_NOTBOL);
	}
	if (start !=0) printf("Número total de casamentos = %d\n",i);
}

int main(int argc, char **argv)
{
	if (argc != 3) {
		fprintf(stderr,"Uso: match2 <ER> <input>\n");
		exit(1);
	}
	er_match(argv);
	exit(0);
}

Executando, tem-se a seguinte saída:

  prompt> ./match2 '12' '12012'
  ********** string original **********
  12012
  
  início da string de pesquisa atual no caractere 0
  string de pesquisa = "12012"
  casou do caractere = 0 ao 2
  
  início da string de pesquisa atual no caractere 2
  string de pesquisa = "012"
  casou do caractere = 1 ao 3
  
  Número total de casamentos = 2
  
  
  prompt> 
  prompt> 
  prompt> ./match2 '2|6' '1234567890'
  ********** string original **********
  1234567890
  
  início da string de pesquisa atual no caractere 0
  string de pesquisa = "1234567890"
  casou do caractere = 1 ao 2
  
  início da string de pesquisa atual no caractere 2
  string de pesquisa = "34567890"
  casou do caractere = 3 ao 4
  
  Número total de casamentos = 2

E como último exemplo do tutorial, um grep colorido, onde o programa mostra somente as linhas que casarem com a ER e coloca as partes que casarem com a ER em uma cor diferente.

/*
 * grep_colorido.c
 *
 * imprime somente as linhas que casarem com a expressão regular
 * passada e colore a parte da linha que casa com a ER.
 * 
 * argv[1] = expressão regular
 * argv[2] = arquivo
 *
 * ex: ./grep_colorido '^er' arquivo.txt
 */

/* headers necessários */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>

/* caracteres de controle 
 * cor azul */
#define START_COLOR "\3[6;1m"
#define END_COLOR   "\3[m"

/* mostra uma mensagem do erro usando regerror */
int er_error(int i, regex_t reg)
{
	size_t length;
	char *buffer=NULL;

	/* pega o tamanho da mensagen de erro */
	length = regerror (i, &reg, NULL, 0);

	/* aloca espaço para a mensagem de erro */
	if ((buffer = (char *)malloc (length)) == NULL) {
		fprintf(stderr, "error: malloc buffer\n");
		exit(1);
	}
	
	/* coloca em buffer a mensagem de erro */
	regerror (i, &reg, buffer, length);
	
	fprintf(stderr,"Erro: %s\n",buffer);
	free(buffer);
	exit(1);
}

/* lê uma linha inteira do arquivo */
int get_line(char **line, FILE *fp)
{
	int tam=2;
	int start=0;

	if ((*line = (char *) malloc(tam)) == NULL) {
		fprintf(stderr, "Erro: malloc line\n");
		exit(1);
	}
	while (fgets(*line+start, tam, fp) != NULL) {
		if (strchr(*line, &#9;\n&#9;) != NULL)
			return 1;
		start += tam-1;
		*line = (char *)realloc(*line, start+1+tam);
	}
	return -1;
}

/* tenta casar uma ER com o input */
void er_match(char *er, FILE *fp)
{
	int i,error, start;
	char *line = NULL;
	/* aloca espaço para a estrutura do tipo regmatch_t */
	regmatch_t match;
	/* aloca espaço para a estrutura do tipo regex_t */
	regex_t reg;

	/* compila a ER passada em argv[1]
	 * em caso de erro, a função retorna diferente de zero */
	if ((i=regcomp(&reg , er, REG_EXTENDED|REG_NEWLINE)) != 0)
		/* imprime uma string do erro */
		er_error(i,reg);	

	/* lê o arquivo linha por linha */
	while ((i = get_line(&line, fp)) != -1) {
		/* coloca o offset para o início da linha */
		start = 0;
		error = regexec(&reg, line, 1, &match, 0);
		/* enquanto a linha casar com a ER */
		while (error == 0) {
			/* imprime do início da string até o caractere antes do match */
			fwrite(line+start, 1, match.rm_so, stdout);
			printf("%s",START_COLOR); /* caracteres de controle */
			/* imprime a parte da string que casa com a ER */
			fwrite(line+start+match.rm_so, 1, match.rm_eo - match.rm_so, stdout);
			printf("%s",END_COLOR); /* caracteres de controle */
			
			/* atualiza o offset de início da string */
			start += match.rm_eo;
			error = regexec(&reg, line+start, 1, &match, REG_NOTBOL);
		}
		/* caso ocorreu algum match, se necessário, imprime o resto da linha */
		if (start != 0) fwrite(line+start, 1, strlen(line+start), stdout);
		/* caso queira imprimir todo o arquivo, comente a linha com o if
		 * acima e descomente a próxma linha 
		fwrite(line+start, 1, strlen(line+start), stdout); */
	}
}

int main(int argc, char **argv)
{
	FILE *fp;

	if (argc != 3) {
		fprintf(stderr,"Uso: grep_colorido <ER> <arquivo>\n");
		exit(1);
	}
	if ((fp = fopen(argv[2], "r")) == NULL) {
		fprintf(stderr,"Erro ao abrir %s\n",argv[2]);
		exit(1);
	}
	er_match(argv[1], fp);
	fclose(fp);
	exit(0);
}

  prompt> echo -e "ABCD\nEFG\nHIABZ"
  ABCD
  EFG
  HIABZ
  prompt> 
  prompt> ./grep_colorido 'AB' <(echo -e "ABCD\nEFG\nHIABZ")
  ABCD
  HIABZ

PS: note que o 'grep_colorido' espera como argv[2] um arquivo (file descriptor). O comando utilizado no exemplo só funciona porque a estrutura do tipo '<()' é expandida para um fd (file descriptor) pelo shell.

Considerações Finais

Espero que este texto tenha sido útil para você e que consiga tirar proveito de ER em seus programas C.

Baixe todos os programas do tutorial: prog_er.tgz

Ah, para colocar o código em C coloridinho, bonitinho, bem fru-fru, fiz um pequeno script em sed. Se quiser baixar colorize_c.sed

NOTA: Conversando com o Aurélio sobre o colorize_c.sed, ele me falou que o próprio VIM faz isso, ie, faz um 2html com a 'syntax highlighting' atual para as linguagens que ele suporta (ls /usr/share/vim/vim*/syntax/2html.vim). De qualquer maneira foi divertido fazer o sed. 8=)


This HTML page is (see source)