Precisa de uma linha de comando? Use a GNU readline library!

Recentemente precisei criar uma aplicação, em modo texto, que emulava uma linha de comando para o usuário. Inicialmente implementei a rotina usando fgets, lendo o stream stdin. Funcionava que era uma beleza. Depois, quis implementar recursos com o history e autocompletion

Descobri a maravilhosa library GNU readline e GNU history. Dêem uma olhada no código:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <malloc.h>
#include <readline/readline.h>
#include <readline/history.h>

#define MAX_HISTORY_ENTRIES 32

/* Our history memory management structures. */
int numHistoryEntries = 0;
char *historyEntries[MAX_HISTORY_ENTRIES] = { NULL };

/* Prototypes. */
void HistoryCleanup(void);
void signalHandler(int);
void AddHistoryEntry(char *);

int main(void)
{
  struct sigaction sa;
  char *line, *p;

  /* Let us treat our own signals! */
  rl_catch_signals = 0;

  /* TIP: Disablea autocompletion binding TAB key */
  rl_bind_key('\t', rl_abort);

  /* History will keep the last 32 lines. */
  stifle_history(MAX_HISTORY_ENTRIES);

  /* Setup our signal handler.
     This method is preferable than use signal() */
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = signalHandler;
  sigaction(SIGINT, &sa, NULL);

  /* Read lines loop! */
  for (;;)
  {
    /* if a line is typed... */
    if ((line = readline("> ")) != NULL)
    {
      /* Get rid of the final '\n' char! */
      p = strchr(line, '\n');

      if (p != NULL)
        *p = '\0';

      /* QUIT or quit exits */
      if (strcasecmp(line, "quit") == 0)
        break;

      AddHistoryEntry(line);
      printf("Line: \"%s\".\n", line);
    }
  }

  /* Get rid of the last line! */
  free(line);

  /* Get rid of all lines on history. */
  HistoryCleanup();

  return 0;
}

/* Clean up all history entries */
void HistoryCleanup(void)
{
  int i;

  for (i = 0; i < numHistoryEntries; i++)
    free(historyEntries[i]);
}

/* Handles SIGINT */
void signalHandler(int signal)
{
  switch (signal)
  {
  case SIGINT:
    /* Put readline in a clean state after Ctrl+C. */
    rl_free_line_state();
    rl_cleanup_after_signal();

    /* HACK: Shows ^C and skip to next line! */
    puts("^C");
    break;
  }
}

/* This is necessary since readline allocate a buffer
   and stifled history didn't free older lines. */
void AddHistoryEntry(char *line)
{
  int index;

  index = numHistoryEntries + 1;

  /* Frees the first entry if
     buffer is full. */
  if (index == MAX_HISTORY_ENTRIES)
  {
    free(historyEntries[0]);

    /* And move the entire buffer
       1 position back! */
    memcpy(historyEntries,
           &historyEntries[1],
           sizeof(char *) * numHistoryEntries);
  }

  /* Store the line */
  historyEntries[numHistoryEntries] = line;

  if (index < MAX_HISTORY_ENTRIES)
    numHistoryEntries++;

  /* put the line in the readline history! */
  add_history(line);
}

A função readline aloca o buffer com a linha lida pra você. A única coisa que você tem que lembrar é de se livrar desse buffer usando a função free. O código acima usa a library GNU History para guardar as últimas 32 linhas digitadas e, para isso, tive que implementar uma rotina de memory management para não causar memory leaks. A cada linha adicionada no histórico mantenho o ponteiro do buffer numa lista (historyEntries) e me livro da primeira da lista sempre que a lista estiver cheia. Infelizmente GNU History não faz isso pra você… Tudo o que ela faz é “esquecer” os primeiros itens do histórico (cujo tamanho é indicado em stifle_history. O buffer continua alocado e é seu trabalho livrar-se dele.

Note que a rotina também implementa um tratador de sinais para lidar com o sinal SIGINT (o sinal gerado pelo Ctrl+C). Se você não informar à library GNU Readline que ela não deve tratar sinais, através da variável rl_catch_signals, vocẽ não será capaz de capturar SIGINT no seu próprio código (já que readline fará isso).

A função readline também tratará automaticamente o autocompletion, no mesmo estilo da linha de comando do bash. Para evitar isso é necessário remapear a tecla TAB para que essa não faça coisa alguma. Isso é feito com rl_bind_key. Vocẽ pode ignorar isso se implementar o seu próprio esquema de autocompletion (e existem funções para isso em GNU Readline).

Por default a função readline usará as definições contidas no arquivo de configuração /etc/inputrc, mas vocẽ pode escrever o seu e usar uma das funções de inicialização da library para usá-lo.

Para compilar o código acima é necessário instalar o pacote libreadline6-dev (instalar o meta-pacote libreadline.dev é suficiente). Para distribuir sua aplicação que use GNU Readline Library e GNU History Library é necessário colocar a dependência do pacote libreadline6 (não existe meta-pacote libreadline!). Assim, compilando o código acima:

$ sudo apt-get -y install libreadline6-dev 
$ gcc -o cmdline cmdline.c -lreadline

Dê uma olhada na documentação da GNU Readline Library aqui.

PS: Notaram que eu não usei a função signal para instalar o tratador para SIGINT? A descrição da manpage a respeito de signal (“man 2 signal“) nos avisa que devemos evitar o uso de signal e usar sigaction, porque o comportamento da primeira é dependente de arquitetura.

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