Um Prompt Interativo • Capítulo 4

Leia, Avalie, Imprima


reptile

Réptil • Soa quase igual REPL

À medida que construímos nossa linguagem de programação precisaremos de alguma maneira de interagir com ela. C usa um compilador, onde você muda o seu programa, recompile e roda-o. Seria bom se pudéssemos fazer algo melhor, para interagir com a linguagem de maneira dinâmica, de maneira que possamos testar seu comportamento em diversas condições bem rapidamente. Para isso, construiremos algo chamado prompt interativo.

Isso é um programa que solicita (em inglês, prompts) alguma entrada ao usuário, e quando esta for fornecida, ele responde de volta com alguma mensagem. Usar isso será a maneira mais fácil de testar nossa linguagem de programação e ver como ela age. Este sistema é também chamado de REPL, que significa read-evaluate-print loop (laço leia-avalie-imprima). É uma maneira comum de interagir com uma linguagem de programação, que você talvez tenha usado antes em linguagens como Python ou Ruby.

Antes de construir um REPL completo, começaremos com alguma coisa mais simples. Vamos fazer um sistema que solicita entrada, e ecoa qualquer entrada de volta. Se fizermos isto, podemos depois estendê-lo para analisar a entrada do usuário e avaliá-la, como se fosse um verdadeiro programa Lisp.

Um prompt interativo


Para a construção básica, queremos escrever um laço que repetidamente escreve uma mensagem, e então espera por alguma entrada. Para obter entrada do usuário, podemos usar uma função chamada fgets, que lê qualquer entrada até uma quebra de linha. Precisamos de algum lugar para armazenar essa entrada do usuário. Para isso vamos declarar um buffer de entrada com tamanho constante.

Uma vez que tenhamos a entrada do usuário armazenada, podemos imprimi-la de volta usando uma função chamada printf.

#include <stdio.h>

/* Declara um buffer para entrada com tamanho 2048 */
static char input[2048];

int main(int argc, char** argv) {

  /* Imprime informacao sobre versao e como sair */
  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");

  /* Num laco que nunca acaba */
  while (1) {

    /* Imprima nosso prompt */
    fputs("lispy> ", stdout);

    /* Leia uma linha de entrada do usuario com tamanho maximo 2048 */
    fgets(input, 2048, stdin);

    /* Ecoa a entrada de volta para o usuario user */
    printf("No you're a %s", input);
  }

  return 0;
}

O que é aquele texto em verde fraco?

O código acima contem comentários. Estes são seções do código entre símbolos /* */, que são ignorados pelo compilador, mas são usados para informar a pessoa lendo o que está acontecendo. Preste atenção neles!

Vamos repassar esse programa com um pouco mais de profundidade.

A linha static char input[2048]; declara um array global de 2048 caracteres. Isto é um bloco reservado de dados que podemos acessar de qualquer lugar do nosso programa. Nele vamos armazenar a entrada do usuário que é digitada na linha de comando. A palavra chave static torna essa variável local a este arquivo, e a seção [2048] é o que declara o seu tamanho.

Nós escrevemos um laço infinito usando while (1). Em um bloco condicional, 1 sempre avalia como verdadeiro. Por isso, comandos dentro desse laço vão executar para sempre.

Para imprimir nosso prompt, usamos a função fputs. Esta é uma pequena variação de puts que não acrescenta um caractere de quebra de linha. Usamos a função fgets para obter entrada do usuário da linha de comando. Ambas funções requerem um arquivo para onde escrever, ou de onde ler. Para isso, fornecemos as variáveis especiais stdin e stdout. Estas são declaradas em <stdio.h> e são variáveis de arquivo especiais representando a entrada e a saída da linha de comando. Quando passamos essa variável, a função fgets espera o usuário digitar uma linha de texto, e quando ele o faz ela armazena-a no buffer input, incluindo o caractere quebra de linha. Para que a fgets não leia dados demais, também fornecemos o tamanho do buffer 2048.

Para ecoar a mensagem de volta ao usuário, usamos a função printf. Esta é uma função que provê uma maneira de imprimir mensagens consistindo de vários elementos. Ela casa argumentos com padrões dentro da string fornecida. Por exemplo, em nosso caso, podemos ver o padrão %s na string dada. Isto significa que ele será substituído por qualquer que seja o próximo argumento passado a seguir, interpretado como uma string.

Para mais informação sobre esses diferentes padrões, veja a documentação sobre printf.

Como é que vou saber sobre funções como fgets e printf?

Não é imediatamente óbvio como saber sobre essas funções padrão, e quando usá-las. Ao confrontar com um problema, é necessário alguma experiência para saber quando este já foi resolvido para você por funções de bibliotecas.

Por sorte C tem uma biblioteca padrão bem pequena e quase todas elas podem ser aprendidas na prática. Se quiser fazer algo que seja bem básico, ou fundamental, vale a pena olhar a documentação de referência para a biblioteca padrão e checar se existem quaisquer funções incluídas que fazem o que você quer.

Compilação


Você pode compilar isso com o mesmo comando que foi usado no segundo capítulo.

cc -std=c99 -Wall prompt.c -o prompt

Depois de compilá-lo, você deve tentar rodá-lo. Você pode usar Ctrl+c para sair do programa quando terminar. Se tudo deu certo, seu programa deve rodar mais ou menos como isso:

Lispy Version 0.0.0.0.1
Press Ctrl+c to Exit

lispy> hello
No You're a hello
lispy> my name is Dan
No You're a my name is Dan
lispy> Stop being so rude!
No You're a Stop being so rude!
lispy>

Editando a entrada


Se você está trabalhando no Linux ou Mac, notará um comportamento estranho caso tente usar as setas do teclado para tentar editar a entrada.

Lispy Version 0.0.0.0.3
Press Ctrl+c to Exit
  
lispy> hel^[[D^[[C           

Usar as setas está criando estes caracteres estranhos ^[[D ou ^[[C, em vez de mover o cursor pela entrada. O que realmente queremos é ser capaz de mover em torno da linha, deletando e editando a entrada caso tenhamos cometido algum erro.

No Windows este comportamento é o padrão. No Linux e no Mac isso é providenciado por uma biblioteca chamada editline. No Linux e no Mac, precisamos substituir nossas chamadas para fputs e fgets com chamadas para as funções que esta biblioteca fornece.

Caso esteja desenvolvendo no Windows e queira simplesmente continuar, fique à vontade para pular para o fim deste capítulo já que as próximas seções podem não ser relevantes.

Usando a Editline

A biblioteca editline fornece duas funções que vamos usar chamadas readline (leia linha) e add_history (adicione histórico). A primeira função, readline é usada para ler entrada de algum prompt, permitindo editar a entrada. A segunda função add_history nos permite gravar o histórico das entradas de maneira que elas possam ser obtidas com as setas para cima e para baixo.

Nós substituímos fputs e fgets com chamadas para essas funções para obter o seguinte:

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

#include <editline/readline.h>
#include <editline/history.h>

int main(int argc, char** argv) {
   
  /* Imprime informacao sobre versao e como sair */
  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");
   
  /* Num laco que nunca acaba */
  while (1) {
    
    /* Imprima nosso prompt e obtem entrada */
    char* input = readline("lispy> ");
    
    /* Adiciona a entrada ao historico */
    add_history(input);
    
    /* Ecoa a entrada de volta ao usuario */    
    printf("No you're a %s\n", input);

    /* Libera a entrada obtida */
    free(input);
    
  }
  
  return 0;
}

Nós incluímos alguns novos cabeçalhos (headers). Há o #include <stdlib.h>, que nos dá acesso à função free usada mais tarde no código. Também adicionamos #include <editline/readline.h> e #include <editline/history.h> que nos dá acesso às funções editline, readline e add_history.

Em lugar de solicitar a entrada, e pegá-la com fgets, fazemos ambos em uma vez usando apenas readline. O resultado disso nós passamos para add_history para gravá-lo. Finalmente o imprimimos como antes, usando printf.

Diferentemente da fgets, a função readline retira o caractere quebra-de-linha à direita da entrada, então precisamos adicioná-lo na chamada à função printf. Também precisamos deletar a entrada nos dada pela função readline usando free. Isto é porque diferentemente da fgets, que escreve para um buffer existente, a função readline aloca nova memória quando é chamada. Quando devemos liberar memória é algo que vamos cobrir em profundidade em capítulos posteriores.

Compilando com a Editline

Se tentar compilar isto imediatamente com o comando anterior você obterá um erro. Isto é porque primeiro precisamos instalar a biblioteca editline no seu computador.

fatal error: editline/readline.h: No such file or directory #include <editline/readline.h>

No Mac a biblioteca editline vem com Command Line Tools (ferramentas da linha de comando). Instruções para instalação podem ser encontradas no Capítulo 2. Você pode ainda receber um erro sobre o cabeçalho de histórico (history header) não ser encontrado. Neste caso, remova a linha #include <editline/history.h>, pois esta pode não ser necessária.

No Linux você pode instalar a editline com sudo apt-get install libedit-dev. No Fedora, pode usar o comando su -c "yum install libedit-dev*".

Tendo instalado a editline, você pode tentar compilar novamente. Dessa vez obterá um erro diferente.

undefined reference to `readline'
undefined reference to `add_history'

Isto significa que você não vinculou (em inglês, linked) seu programa com a editline. Este processo de vinculação (ligação, ou linking) permite ao compilador diretamente embutir chamadas para a editline ao seu programa. Você pode fazê-lo ligar adicionando a opção -ledit ao seu comando de compilar, antes da opção de saída -o).

cc -std=c99 -Wall prompt.c -ledit -o prompt

Rode isso e verifique se agora consegue editar a entrada à medida que você digita.

Ainda não está funcionando!

Alguns sistemas podem ter pequenas variações em como instalar, incluir e vincular com editline. Por exemplo, no Arch Linux o cabeçalho history da editline é histedit.h. Caso esteja tendo problemas, pesquise online e veja se consegue encontrar instruções específicas para sua distribuição em como instalar e usar a biblioteca editline. Se isso não der certo, busque por instruções para a biblioteca readline, que é um substituto para a editline. No Mac ela pode ser instalada usando HomeBrew ou MacPorts.

O pré-processador C


Para um projeto tão pequeno, pode ser tranquilo ter que programar diferentemente dependendo de qual sistema operacional estamos usando, mas se queremos enviar o código fonte a um amigo em um sistema operacional diferente para me ajudar com a programação isso vai causar problemas. Num mundo ideal eu gostaria que meu código fonte fosse capaz de compilar não importa onde ou em qual computador ele está sendo compilado. Este é um problema geral em C, e é chamado portabilidade. Nem sempre há uma solução fácil ou correta.

octopus

Octopus • Soa quase igual Octothorpe #

Mas C fornece um mecanismo para ajudar, chamado o pré-processador.

O pré-processador é um programa que roda antes do compilador. Ele tem uma gama de propósitos, e nós já o estamos usando sem saber. Qualquer linha que comece com um caractere octothorpe/jogo-da-velha/sustenido # é um comando do pré-processador. Nós o estamos usando para incluir (include) arquivos de cabeçalho, nos dando acesso às funções da biblioteca padrão e outras.

Um outro uso do pré-processador é detectar qual sistema operacional o código está sendo compilado, e usar isto para emitir código diferente.

Isto é exatamente como nós vamos fazê-lo. Se estivermos rodando Windows, vamos deixar o pré-processador emitir código com algumas funções que preparei imitando readline e add_history, senão vamos incluir os cabeçalhos da editline e usá-los.

Para declarar qual código o compilador deve emitir, podemos envolvê-lo em comandos do pré-processador #ifdef, #else, e #endif. Estes são como uma função if que acontece antes do código ser compilado. Todo o conteúdo do arquivo desde o primeiro #ifdef ao próximo #else é usado se a condição for verdadeira, caso contrário todo o conteúdo desde o #else até o último #endif será usado no lugar. Colocando isso em torno das nossas funções imitadoras, e de nossos cabeçalhos da editline, o código que será emitido deverá compilar no Windows, Linux ou Mac.

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

/* Caso estivermos compilando no windows, compile estas funcoes */
#ifdef _WIN32
#include <string.h>

static char buffer[2048];

/* Funcao imitadora readline */
char* readline(char* prompt) {
  fputs(prompt, stdout);
  fgets(buffer, 2048, stdin);
  char* cpy = malloc(strlen(buffer)+1);
  strcpy(cpy, buffer);
  cpy[strlen(cpy)-1] = '\0';
  return cpy;
}

/* Funcao imitadora add_history */
void add_history(char* unused) {}

/* Senao, inclua os cabecalhos da editline  */
#else
#include <editline/readline.h>
#include <editline/history.h>
#endif

int main(int argc, char** argv) {
   
  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");
   
  while (1) {
    
    /* Agora em qualquer caso readline vai estar corretamente definida */
    char* input = readline("lispy> ");
    add_history(input);

    printf("No you're a %s\n", input);
    free(input);
    
  }
  
  return 0;
}

Referência


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

#include <editline/readline.h>
#include <editline/history.h>

int main(int argc, char** argv) {
   
  /* Print Version and Exit Information */
  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");
   
  /* In a never ending loop */
  while (1) {
    
    /* Output our prompt and get input */
    char* input = readline("lispy> ");
    
    /* Add input to history */
    add_history(input);
    
    /* Echo input back to user */    
    printf("No you're a %s\n", input);

    /* Free retrived input */
    free(input);
    
  }
  
  return 0;
}
#include <stdio.h>

/* Declare a buffer for user input of size 2048 */
static char input[2048];

int main(int argc, char** argv) {

  /* Print Version and Exit Information */
  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");

  /* In a never ending loop */
  while (1) {

    /* Output our prompt */
    fputs("lispy> ", stdout);

    /* Read a line of user input of maximum size 2048 */
    fgets(input, 2048, stdin);

    /* Echo input back to user */
    printf("No you're a %s", input);
  }

  return 0;
}
#include <stdio.h>
#include <stdlib.h>

/* If we are compiling on Windows compile these functions */
#ifdef _WIN32
#include <string.h>

static char buffer[2048];

/* Fake readline function */
char* readline(char* prompt) {
  fputs(prompt, stdout);
  fgets(buffer, 2048, stdin);
  char* cpy = malloc(strlen(buffer)+1);
  strcpy(cpy, buffer);
  cpy[strlen(cpy)-1] = '\0';
  return cpy;
}

/* Fake add_history function */
void add_history(char* unused) {}

/* Otherwise include the editline headers */
#else
#include <editline/readline.h>
#include <editline/history.h>
#endif

int main(int argc, char** argv) {
   
  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");
   
  while (1) {
    
    /* Now in either case readline will be correctly defined */
    char* input = readline("lispy> ");
    add_history(input);

    printf("No you're a %s\n", input);
    free(input);
    
  }
  
  return 0;
}

Metas bônus


  • › Mude o prompt de lispy> para algo de sua escolha.
  • › Mude o que é ecoado de volta ao usuário.
  • › Adicione uma mensagem extra na informação de versão e como sair.
  • › O que \n significa naquelas strings?
  • › Que outros padrões podem ser usados com printf?
  • › O que acontece quando você passa para printf uma variável que não casa com o padrão?
  • › O que faz o comando do pré-processador #ifndef?
  • › O que faz o comando do pré-processador#define?
  • › Se _WIN32 está definido no Windows, o que está definido no Linux ou Mac?

Navegação

‹ O Básico

• Conteúdo •

Linguagens ›