C: Lendo arquivos de configuração

Esse é um programa mostrando como implementar a leitura de arquivos de configuração simples em C, com “variáveis” sendo criadas na hora. Foi esse o código que eu usei no artigo sobre optimizações e que acabou me surpreendendo por as listas serem mais rápidas que árvores binárias neste caso.

Talvez se na função main() nós realizássemos mais buscas, depois de um tempo as árvores começassem a ganhar, mas ainda sim, as árvores foram duas vezes mais lentas que listas.

De qualquer forma, o código. Primeiro, as definições básicas:

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

enum errors {
  ERR_NO_EQ = -0x001,

  ERR_ALLOC_OPT = -0x010,
  ERR_ALLOC_STR = -0x020,
  ERR_OPEN_FILE = -0x040,
};

struct option {
  char *var;
  char *value;
  struct option *next;
};

/* Returns the position of the '=' char in a string. Useful for splitting it in
 * two. find_eq_pos will return ERR_NO_EQ if no '=' is found so you can ignore
 * it or handle it differently. */
int find_eq_position(const char *line);

/* Returns a string without comments */
void trim_comments(char *line);

/* Returns a trimmed string (with no trailing or leading spaces). Eg
 * 'option=value' == '  option  = value '. It's not very good, memorywise, but
 * it works. */
char *trim_white_space(char *line);

/* Adds a new node at the start of the list. Returns 0 on success or
 * ERR_ALLOC_OPT if there were a problem when calling malloc. */
int new_option(struct option **options, char *var, char *value);

/* Reverses the list. Useful because as we push() things, they will be in the
 * opposite order from the config file, so if we want to access it in the same
 * way, we should reverse it. Returns the reversed list. */
struct option *reverse(struct option *begin);

/* Parses a line and, if successful, add it to the options list. It may return
 * ERR_ALLOC_OPT or ERR_ALLOC_STR, which says there were a problem calling
 * malloc, or 0, success */
int parse_line(struct option **options, char *line);

/* Searches for the option named opt_name. If none is found, returns NULL, if
 * finds, returns the list node whose var name is opt_name. This is useful as
 * there may be more than one option with the same opt_name, so you can search
 * again, running get_opt with return->next. Don't forget, however, that it
 * returns the original node; changes directly made directly to it may corrupt
 * your opts list. */
struct option *get_option(struct option *options, const char *option_name);

/* Parses the file. */
int parse_file(struct option **options, const char *file_name);

int main(int argc, char *argv[])
{
  struct option *options = NULL;
  struct option *current = NULL;

  if(argc == 1)
  {
    parse_file(&options, "file-config.cfg");
  } else {
    parse_file(&options, argv[1]);
  }

  current = options;
  while(current != NULL)
  {
    printf("%s: %s\n", current->var, current->value);
    current = current->next;
  }

  current = options;
  while( (current = get_option(current, "message")) ) /* yes, it's a =, not a == */
  {
    printf("%s found: %s\n", current->var, current->value);
    current = current->next;
  }

  return(0);
}

int find_eq_position(const char *line)
{
  int count = 0;

  while(*(line+count) != '')
  {
    if(*(line+count) == '=')
    {
      return(count);
    }
    count++;
  }

  return(ERR_NO_EQ);
}

void trim_comments(char *line)
{
  int count = 0;

  while(*(line+count) != '')
  {
    if(*(line+count) == '#')
    {
      *(line+count) = '';
      return;
    }

    count++;
  }
}

char *trim_white_space(char *line)
{
  char *end = NULL;

  while(*line == ' ' || *line == '\t' || *line == '\n' || *line == '\r')
  {
    line++;
  }

  end = line + strlen(line) - 1;
  while(end > line &&
                (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r'))
  {
    end--;
  }

  *(end+1) = '';
  return(line);
}

int new_option(struct option **options, char *var, char *value)
{
  struct option *new = malloc(sizeof(struct option));
  if(new == NULL)
  {
#ifdef DEBUG
    fprintf(stderr, "error allocating %u bytes of memory in 'new_option'\n",
            sizeof(struct option));
#endif
    return(ERR_ALLOC_OPT);
  }

  new->var = var;
  new->value = value;
  new->next = *options;

  *options = new;

  return(0);
}

struct option *reverse(struct option *begin)
{
  struct option *prev = NULL;
  struct option *curr = begin;
  struct option *next = NULL;

  /* if begin is null, there is no next (segfault) */
  if(begin==NULL)
  {
    return(NULL);
  }
  next = curr->next;

  /* if next is null, this is a single list. return as it is. */
  if(next==NULL)
  {
    return(curr);
  }

  /* otherwise, reverse it */
  while(next!=NULL)
  {
    next = curr->next;
    curr->next = prev;
    prev = curr;
    curr = next;
  }

  return(prev);
}

int parse_line(struct option **options, char *line)
{
  char *var = NULL;
  char *value = NULL;
  char *linecpy = NULL;

  int position = 0;
  int line_size = 0;

  trim_comments(line);

  position = find_eq_position(line);
  if(position == ERR_NO_EQ)
  {
    return(ERR_NO_EQ);
  }

  line_size = strlen(line);
  linecpy = calloc(line_size, sizeof(char)*line_size);
  if(linecpy==NULL)
  {
#ifdef DEBUG
    fprintf(stderr, "error allocating %u bytes of memory in 'parse_line'\n",
            sizeof(char)*line_size);
#endif
    return(ERR_ALLOC_STR);
  }

  strcpy(linecpy, line);

  *(linecpy+position) = '';

  var = trim_white_space(linecpy);
  value = trim_white_space(linecpy + position + 1);

  return(new_option(options, var, value));
}

struct option *get_option(struct option *options, const char *option_name)
{
  struct option *current = options;

  while(current != NULL)
  {
    if(strcmp(current->var, option_name) == 0)
    {
      return(current);
    }
    current = current->next;
  }

  return(NULL);
}

int parse_file(struct option **options, const char *file_name)
{
  char line[256];
  FILE *fptr = NULL;

  fptr = fopen(file_name, "r");
  if(fptr == NULL)
  {
#ifdef DEBUG
    fprintf(stderr, "error openning file %s in 'parse_file'\n", file_name);
#endif
    return(ERR_OPEN_FILE);
  }

  while(fgets(line, 255, fptr) != NULL)
  {
    int ret = parse_line(options, line);
    if(ret == ERR_ALLOC_OPT || ret == ERR_ALLOC_STR)
    {
#ifdef DEBUG
      fprintf(stderr, "error allocating memory in 'parse_file' (%x)\n", ret);
#endif
      fclose(fptr);
      return(ret);
    }
  }

  *options = reverse(*options);

  fclose(fptr);
  return(0);
}

Primeiro nós criamos um enum que funcionará mais ou menos como um #define. A vantagem de usar enum é que o nome da variável permanece nó código após a passagem do pré-processador. Ou seja, se acontecer algum erro, nós teremos o nome da variável e não apenas um número como aconteceria com #define

Depois nós definimos a estrutura básica do nosso código, a opção. A string var guarda o nome da “variável”, e value o valor, de forma que se nós fossemos criar uma opção já com um valor, ficaria algo assim:

struct option teste;
teste.var = "resposta";
teste.value = "42";
teste.next = NULL;

Como estamos trabalhando com listas de tamanho dinâmico, trabalhamos com ponteiros para essas structs, mas a idéia permanece a mesma. Uma única alteração é que ao invés de um . usaremos um ->.

Então nós temos as funções que fazem o trabalho. Vou descrever o que cada uma faz, os argumentos que ela aceita e o mecanismo envolvido (talvez eu devesse começar a fazer comentários bilíngües nos meus arquivos :P).

  • int find_eq_position(const char* line): Esta função percorre a string line em busca de um ‘=’ e retorna a posição em que ele foi encontrado. Caso ela não encontre nenhum ‘=’, ela retorna ERR_NO_EQ.

    Ela serve para nos dizer onde separar a string em nome da variável e valor da variável. Um detalhe de sua implementação que pode causar estranheza em alguns é o uso de aritmética de ponteiros. line aponta para o primero char dessa string, então line+1 para o seguinte, e assim sucetivamente. Outro detalhe é o uso do * já que queremos comparar o valor para qual o ponteiro aponta, e não o endereço.

  • void trim_comments(char *line): Esta altera line, retirando qualquer comentário que possa aparecer nela. O funcionamento dela é bastante simples: como C considera o caractere nulo como o fim de uma string, nós trocamos o # do comentário por um caractere nulo e o resto da linha é ignorado.

    Isso permite que no arquivo tenhamos tanto comentários no início da linha como depois de uma opção:

    # Este é um comentário que ocupa toda a linha
    opção = valor # uma explicação qualquer sobre essa opção
  • char *trim_white_space(char *line): Essa tira espaços desnecessários no início e no fim de uma string (mas não entre palavras). Assim,
           opção       =      valor     

    equivale a

    opção = valor

    mas

    opção = valor     com      muitos      espaços

    continua como é.

    Ela funciona “empurrando” o início da string para o primeiro caractere que não seja branco da mesma, e colocando um caractere nulo logo após o primeiro caractere não-branco contando a partir do fim.

  • int new_option(struct option **options, char *var, char *value): Esta “empurra” uma nova opção no início de uma lista de opções. É exatamente o que uma função push() genérica faz.

    A única coisa de diferente nela é a presença de

    #ifdef DEBUG
    ...
    #endif

    Isso quer dizer que se você compilar o código com a opção -DDEBUG e houver um erro, ela imprimirá aquela mensagem, se não, não.

    Além disso (seja com DEBUG definido ou não), ela retorna ERR_ALLOC_OPT caso ela não consiga alocar a memória para criar uma opção.

  • struct option *reverse(struct option *begin): esta retorna a lista que começa por begin invertida. Ela não é de fato necessária para o código, mas como usamos uma função do tipo push para criar novas opções, elas aparecem na ordem inversa em que estavam no arquivo.
  • int parse_line(struct option **options, char *line): esta pega a string line, tenta criar uma nova opção a partir dela e se conseguir, a insere nas options. Ela retira os comentários, tenta achar um ‘=’ e divide uma cópia da string naquele ponto.

    Essa pode retornar 3 erros: ERR_NO_EQ, ERR_ALLOC_OPT ou ERR_ALLOC_STR, dependendo da chamada para as outras funções.

  • struct option *get_option(struct option *options, const char *option_name): Essa procura pela option cujo nome da variável seja option_name. A vantagem de ela retornar a option e não apenas o valor dela é que você pode fazer como na função main() e buscar mais de uma função com o mesmo nome.
  • int parse_file(struct option **options, const char *file_name): Esta aqui é a função principal. Ela abrirá o arquivo dado e criará a lista de opções. Embora possa parecer estranho ter de passar options como argumento, isso facilita se você quiser ler mais de um arquivo, mas queira ter só uma lista com todas as opções.

E por último temos a função main() com alguns exemplos de uso. Se nenhum comentário for passado, ela tenta abrir o arquivo file-config.cfg, ou então ela tenta abrir o argumento passado. Depois ela imprime todas as opções como foram lidas e busca por uma opção chamada message.

Para o teste eu usei outra main(), mas eu a coloco no próximo artigo junto com os patches (sim, foi por isso que eu escrevi aquele artigo ontem :P).

Publicidade

2 Respostas para “C: Lendo arquivos de configuração

  1. Pingback: Notas sobre optimizações « Aletéia

  2. Pingback: Notas sobre optimizações II: O código « Aletéia

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo 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 )

Conectando a %s

%d blogueiros gostam disto: