Artigos aprofundados sobre a tecnologia que molda o que vem a seguir.

O que construir um shell ensina sobre Unix

Construir um shell do zero revela como o Unix funciona: processos, descritores de arquivo, pipes e sinais. Um passo a passo pelos conceitos centrais.

Concha do mar cortada revelando uma sala de controle com tubos, válvulas e alavancas que direcionam a luz.

Todo desenvolvedor usa um shell diariamente. Poucos entendem de fato o que ele faz. O shell parece uma aplicação — você digita comandos, ele os executa —, mas na prática é uma camada fina sobre um conjunto de primitivas do Unix que são fundamentais para o funcionamento dos sistemas operacionais. Construir um shell do zero é uma das melhores formas de entender processos, descritores de arquivo, pipes e sinais — conceitos que sustentam tudo, de servidores web a contêineres Docker.

Um shell básico é surpreendentemente simples. O loop central é: ler uma linha de entrada, interpretá-la em um comando e argumentos, fazer fork de um processo filho, executar o comando no filho e esperar que ele termine. São umas 50 linhas de C. A complexidade vem dos recursos que a gente toma como garantidos: pipes, redirecionamento, processos em segundo plano, tratamento de sinais e controle de jobs.

O Loop Read-Eval-Print

No fundo, um shell é um REPL. Lê a entrada, avalia (executa), imprime os resultados e repete. A parte do 'print' fica por conta dos próprios comandos — o shell só fornece o ambiente em que eles rodam.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
// The simplest possible shell
int main(void) {
char line[1024];
while (1) {
printf("$ ");
if (!fgets(line, sizeof(line), stdin))
break;  // EOF (Ctrl+D)
// Remove trailing newline
line[strcspn(line, "\n")] = '\0';
// Fork a child process
pid_t pid = fork();
if (pid == 0) {
// Child: execute the command
execlp(line, line, NULL);  // This only handles single-word commands
perror("exec");
exit(1);
}
// Parent: wait for child to finish
waitpid(pid, NULL, 0);
}
return 0;
}

Este shell de 25 linhas realmente funciona — ele consegue rodar comandos como ls, pwd e date. Não trata argumentos, pipes, redirecionamento nem qualquer outro recurso que você esperaria. Mas demonstra o padrão fundamental: fork, exec, wait.

Fork e Exec: o Modelo de Processos do Unix

A divisão entre fork/exec é a decisão de design mais característica do Unix, e construir um shell faz você entender por que ela existe.

fork() cria uma cópia exata do processo atual. O filho tem a mesma memória, os mesmos arquivos abertos e as mesmas variáveis de ambiente. exec() substitui o programa do filho por um novo. São operações separadas porque o intervalo entre elas — depois do fork, mas antes do exec — é onde o shell configura o ambiente do filho.

Essa é a grande sacada. Quando você digita ls > output.txt, o shell faz fork e, no filho (antes do exec), abre output.txt, redireciona a stdout para ele e então executa o ls. O programa ls não sabe do redirecionamento — ele escreve na stdout como sempre, e a manipulação dos descritores de arquivo feita entre fork e exec direciona essa saída para um arquivo.

// How 'ls > output.txt' works
pid_t pid = fork();
if (pid == 0) {
// Child process — between fork and exec
// Open the output file
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
// Redirect stdout (fd 1) to the file
dup2(fd, STDOUT_FILENO);  // Now fd 1 points to output.txt
close(fd);                 // Close the original fd (no longer needed)
// exec ls — it writes to stdout, which now goes to output.txt
execlp("ls", "ls", NULL);
perror("exec");
exit(1);
}
waitpid(pid, NULL, 0);

Esse design é elegante porque é composável. O filho pode preparar qualquer ambiente antes do exec — redirecionar arquivos, mudar de diretório, alterar variáveis de ambiente, definir limites de recursos, trocar IDs de usuário — e o programa executado herda esse ambiente sem precisar saber de nada disso. Cada comando recebe um ambiente pré-configurado, e o shell é quem faz essa configuração.

Pipes: Conectando Processos

Pipes são o mecanismo de composição mais poderoso do Unix, e implementá-los mostra como o mecanismo por baixo é simples.

A chamada de sistema pipe() cria um par de descritores de arquivo: um para leitura e outro para escrita. Os dados escritos na ponta de escrita aparecem na ponta de leitura. Para implementar ls | grep foo, o shell cria um pipe, faz fork duas vezes, conecta a stdout do ls à ponta de escrita e a stdin do grep à ponta de leitura.

// How 'ls | grep foo' works
int pipefd[2];
pipe(pipefd);  // pipefd[0] = read end, pipefd[1] = write end
pid_t pid1 = fork();
if (pid1 == 0) {
// First child: ls
close(pipefd[0]);              // Don't need read end
dup2(pipefd[1], STDOUT_FILENO); // stdout → pipe write end
close(pipefd[1]);
execlp("ls", "ls", NULL);
exit(1);
}
pid_t pid2 = fork();
if (pid2 == 0) {
// Second child: grep
close(pipefd[1]);              // Don't need write end
dup2(pipefd[0], STDIN_FILENO);  // stdin → pipe read end
close(pipefd[0]);
execlp("grep", "grep", "foo", NULL);
exit(1);
}
// Parent: close both pipe ends and wait
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);

Repare no cuidado em fechar as pontas de descritores não usadas. Isso é crítico: se o pai não fechar as duas pontas do pipe, o grep nunca vai receber EOF na stdin (porque a ponta de escrita continua aberta no pai) e vai ficar travado para sempre. Vazamentos de descritores de arquivo em cadeias de pipes estão entre os bugs mais comuns ao construir um shell.

Comandos Internos (Built-ins)

Alguns comandos não podem ser programas externos. O cd é o exemplo clássico. Se o shell faz fork de um filho e o filho chama chdir(), só o diretório de trabalho do filho muda — o do pai não é afetado, e depois que o filho termina o shell continua no mesmo diretório. Para o cd funcionar, o shell precisa executá-lo no próprio processo, sem fork.

Outros comandos internos incluem export (que modifica o ambiente do shell), exit (que encerra o processo do shell) e source (que executa um script no contexto do shell atual). Todos modificam o estado do próprio shell, o que só pode acontecer no processo dele.

Entender quais comandos são internos e por quê ensina algo fundamental sobre isolamento de processos. Um processo filho não consegue modificar o pai. Isso é uma funcionalidade de segurança, de confiabilidade e, às vezes, um incômodo — mas faz parte do funcionamento básico dos processos no Unix.

Sinais e Controle de Jobs

Aperte Ctrl+C no terminal e o comando em execução para. Parece simples, mas envolve uma interação surpreendentemente complexa entre o terminal, o shell, os sinais e os grupos de processos.

Ctrl+C envia SIGINT para o grupo de processos em primeiro plano. Quem trata isso é o driver do terminal, não o shell. A função do shell é colocar cada comando em seu próprio grupo de processos, para que o SIGINT vá para o comando e não para o shell. Se o shell não configurar os grupos corretamente, o Ctrl+C mata o shell em vez do comando em execução.

O controle de jobs — processos em segundo plano (&), fg, bg, Ctrl+Z — adiciona mais uma camada. O shell precisa rastrear quais processos pertencem a quais jobs, gerenciar os grupos em primeiro e segundo plano e tratar sinais como SIGTSTP (Ctrl+Z, suspender) e SIGCHLD (processo filho terminado). Implementar isso corretamente é a parte mais difícil de construir um shell.

O Que Você Aprende

Construir um shell ensina conceitos que aparecem o tempo todo no desenvolvimento de software, mesmo que você nunca mais escreva código de sistemas.

  • Descritores de arquivo são a interface universal. Arquivos, pipes, sockets, terminais — tudo são descritores de arquivo. Redirecionamento, pipes e comunicação em rede usam o mesmo mecanismo por baixo. Entender isso deixa mais claro tudo, do networking do Docker aos sockets de domínio Unix e às APIs de sistema do Linux.
  • O isolamento de processos é fundamental. Um processo filho não consegue modificar o pai. Variáveis de ambiente, diretório de trabalho e arquivos abertos são cópias herdadas, não referências compartilhadas. É por isso que um cd em um subshell não afeta o pai, e por que contêineres Docker herdam, mas não compartilham, o ambiente do host.
  • Composição vence funcionalidades. O Unix não tem um comando de 'encontrar arquivos que combinam com um padrão e contá-los'. Ele tem find, grep e wc, conectados por pipes. O mecanismo de pipes do shell torna ferramentas simples combináveis em fluxos complexos. Essa filosofia de design — pequenas ferramentas conectadas por interfaces padrão — é o antepassado intelectual dos microsserviços, dos sockets Unix e do design de APIs.
  • Tratamento de erros é, em grande parte, sobre descritores de arquivo. Quando um pipe quebra, quando um processo filho trava, quando a stdin se esgota — o mecanismo por trás é sempre um descritor sendo fechado, um sinal sendo entregue ou um processo saindo com código de status. Depois que você entende o modelo de descritores de arquivo, os padrões de tratamento de erro em sistemas Unix ficam intuitivos.

Por Onde Começar

Se você quer construir um shell, comece pela versão de 25 linhas acima e adicione recursos aos poucos. Primeiro: o parsing de argumentos (dividir a entrada pelos espaços). Depois: redirecionamento de E/S (> e <). Depois: pipes. Depois: comandos internos (cd, exit). Depois: variáveis de ambiente. Cada recurso ensina um conceito novo do Unix, e cada um é um exercício independente.

Use C para o mapeamento mais direto com as chamadas de sistema. Dá para construir um shell em Python ou Rust, mas a implementação em C deixa explícitas as chamadas de sistema subjacentes — você vê exatamente o que fork(), exec(), dup2() e pipe() fazem porque os chama diretamente.

Você não precisa construir um shell de produção. Até um shell de brinquedo, que trate comandos básicos, pipes e redirecionamento, ensina mais sobre como o Unix funciona do que anos usando um. O shell é o programa mais simples que exercita as interfaces mais importantes do sistema operacional — e entender essas interfaces faz de você um desenvolvedor melhor, independentemente da linguagem ou plataforma em que você trabalha.