Objetivo: implementar um interpretador de comandos (shell) minimalista em C, reproduzindo comportamentos essenciais de um terminal Unix/Linux.
Este repositório contém uma implementação do projeto minishell do 42 Cursus. O foco é aprender (na prática) como um shell funciona por dentro: leitura de linha, parsing, expansão de variáveis, criação de processos, pipes, redirecionamentos, sinais, e execução de binários.
- Visão geral
- O que é um shell?
- Requisitos do projeto (42)
- Funcionalidades implementadas
- Como compilar e executar
- Exemplos de uso
- Arquitetura (alto nível)
- Dificuldades comuns e como resolvi
- Nível de aprendizado (42)
- Testes e depuração
- Limitações / não implementado
- Referências
O minishell é um “mini bash”. Ele deve aceitar comandos digitados pelo usuário, interpretá-los e executá-los, incluindo:
- Comandos simples (
ls,cat,grep, etc.) - Pipes (
|) e redirecionamentos (>,>>,<) - Expansão de variáveis de ambiente (
$HOME,$USER,$?) - Builtins (ex.:
cd,echo,pwd,export,unset,env,exit) - Controle básico de sinais (Ctrl-C, Ctrl-, Ctrl-D)
Um shell é um programa que:
- Mostra um prompt
- Lê uma linha do usuário
- Interpreta (tokeniza/parseia) a linha de acordo com regras (aspas, espaços, pipes, redirects)
- Expande variáveis e resolve caminhos
- Executa:
- builtins (executados no processo do shell quando necessário)
- programas externos (via
fork()+execve())
- Espera os processos terminarem e guarda o status de saída
O minishell é um ótimo laboratório para entender como o Linux executa programas e como processos se comunicam.
Em linhas gerais, o projeto pede:
- Um prompt funcionando
- Leitura de linha usando
readline(ou equivalente permitido) - Histórico funcional
- Execução de comandos baseados em
PATHeexecve - Pipes e redirecionamentos
- Aspas simples e duplas
- Variáveis de ambiente e
$? - Builtins obrigatórios
- Tratamento de sinais como no bash
- Não interpretar
\e;(no comportamento clássico do minishell)
Observação: alguns detalhes variam conforme o enunciado/versão. A ideia do README é documentar o que este projeto faz e como.
- O programa exibe um prompt (ex.:
minishell$) - Lê input do usuário
- Lida com
Ctrl-D(EOF): geralmente encerra o shell de forma limpa
Por que isso é difícil?
- O prompt precisa se comportar bem com sinais (Ctrl-C não deve “matar” o shell, só limpar a linha)
readlinetem comportamento específico e precisa ser integrado com sinais e com o estado do terminal
- Armazena comandos digitados (via funcionalidades do
readline) - Permite navegar com as setas (dependendo do terminal)
Um shell precisa transformar texto em uma estrutura executável.
- Lexer: divide a linha em tokens (palavras,
|,>,>>,<,<<, etc.) respeitando aspas. - Parser: monta uma estrutura (ex.: lista de comandos conectados por pipes) e associa redirecionamentos a cada comando.
Pontos críticos:
- Espaços dentro de aspas não quebram tokens:
echo "ola mundo" - Operadores precisam ser reconhecidos:
>>não é>duas vezes - Mensagens de erro para sintaxe inválida:
| cmd,cmd | | cmd,cmd >etc.
Implementadas as expansões típicas do minishell:
- Variáveis de ambiente:
$HOME,$PATH,$USER... - Exit status:
$?
Regras importantes:
- Em aspas simples (
'...') não deve haver expansão. - Em aspas duplas (
"...") deve haver expansão de$VAR.
Pontos críticos:
- Concatenar texto com expansão:
echo "user=$USER" - Variáveis inexistentes viram string vazia
- Suporte a
cmd1 | cmd2 | cmd3 - Criação de pipes com
pipe()e processos comfork() - Redirecionamento de
stdin/stdoutusandodup2()
Pontos críticos:
- Fechar FDs corretamente para não travar (deadlocks)
- Esperar pelos processos (
waitpid) e decidir qual exit status refletir no final
Suporta:
>redireciona stdout (sobrescreve)>>redireciona stdout (append)<redireciona stdin
Pontos críticos:
- Ordem e múltiplos redirecionamentos:
cmd > a > b(o último normalmente “vence”) - Mensagens de erro quando arquivo não abre
- Suporte a
<< LIMITER - Lê linhas até encontrar o
LIMITER - Normalmente implementado com pipe temporário ou arquivo temporário
Pontos críticos:
- Sinais durante o heredoc (Ctrl-C)
- Regras de expansão dentro do heredoc variam com aspas no limiter (dependendo do enunciado)
Builtins clássicos do minishell:
echo(com-n)cd(com caminhos relativos/absolutos)pwdexportunsetenvexit
Por que builtins são especiais?
- Alguns precisam rodar no processo do shell para ter efeito (ex.:
cd,export,unset,exit). - Em pipelines, muitas implementações rodam builtins em processos filhos para manter a semântica do pipe.
- Busca binários em
PATHquando o comando não contém/ - Usa
execve()para executar - Herda e/ou prepara o ambiente (
envp) a partir das variáveis mantidas pelo minishell
Pontos críticos:
- Erros clássicos: “command not found”, “permission denied”, diretório como comando
- Respeitar códigos de saída usados no bash (127, 126, etc.)
Comportamento típico esperado (inspirado no bash):
Ctrl-C(SIGINT):- no prompt: limpa linha e mostra prompt novamente
- durante execução: interrompe o processo em execução
Ctrl-\(SIGQUIT):- geralmente ignorado no prompt
- pode gerar “Quit (core dumped)” em alguns casos (depende da configuração)
Pontos críticos:
- Separar o comportamento do processo pai (shell) e filhos (comandos)
- Configurar handlers com
signal()/sigaction() - Restaurar comportamento padrão nos filhos antes do
execve()
- O minishell mantém uma variável global (ou parte do estado) com o último status
$?deve refletir isso
Regra importante:
- Em pipeline, normalmente o status final é do último comando do pipe (comportamento do bash).
Ajuste os comandos conforme o Makefile do projeto.
gcc/clangmakereadline(geralmentelibreadline-devno Linux)
make./minishellComandos simples:
minishell$ pwd
minishell$ ls -la
minishell$ echo "HOME=$HOME"Pipes:
minishell$ cat file.txt | grep hello | wc -lRedirecionamentos:
minishell$ echo "ola" > out.txt
minishell$ cat < out.txt
minishell$ echo "mais" >> out.txtHeredoc:
minishell$ cat << EOF
linha 1
linha 2
EOFMesmo sem ver tua estrutura interna, um minishell normalmente é organizado assim:
- Loop principal
- mostra prompt
- lê linha
- valida / ignora linha vazia
- Tokenização (lexer)
- gera uma lista de tokens (com tipo e valor)
- Parsing
- transforma tokens em comandos e operadores (pipes/redirects)
- produz uma estrutura (ex.: lista de comandos com argumentos e redirs)
- Expansão
- resolve
$VAR,$? - remove aspas quando necessário
- resolve
- Execução
- se comando é builtin e não está em pipeline: executa no pai
- caso contrário: cria processos, configura pipes, aplica redirs,
execve
- Wait/Status
- coleta status
- atualiza
$?
Problema: lidar com ' e " sem quebrar tokens no meio.
Estratégia:
- Fazer um lexer por estado (fora de aspas / dentro de aspas simples / dentro de aspas duplas)
- Só finalizar token quando encontrar separador válido fora de aspas
Problema: expandir $VAR dentro de "" mas não dentro de ''.
Estratégia:
- Durante o parsing ou etapa de expansão, manter informação sobre o contexto da string
- Aplicar expansão apenas quando permitido
Problema: processos travando por FDs abertos demais.
Estratégia:
- Fechar sempre as pontas do pipe que não são usadas em cada processo
- No pai, fechar FDs assim que não forem mais necessários
Problema: export dentro de pipeline não deve alterar o ambiente do pai.
Estratégia:
- Se existir pipe, executar builtin no filho (efeito local)
- Se não existir pipe, executar no processo pai (efeito persistente)
Problema: Ctrl-C deve agir diferente no prompt vs durante execução.
Estratégia:
- No pai: handler custom para SIGINT
- Nos filhos: restaurar sinais default antes do
execve()
Este projeto é considerado um dos mais importantes do cursus porque força você a consolidar conceitos centrais de sistemas operacionais:
- Processos:
fork,execve,waitpid - Comunicação:
pipe, encadeamento e redirecionamento de fluxo - Arquivos e FDs:
open,dup2,close - Sinais:
SIGINT,SIGQUIT, comportamento pai/filho - Parsing: tokenização, gramática simples, tratamento de erros
- Memória: evitar leaks, gerenciar strings e arrays de forma segura
Se você chegou até aqui e fez tudo funcionando bem (principalmente sem leaks e com sinais corretos), você saiu do projeto com um nível “42” muito mais sólido em:
- leitura e escrita de C
- raciocínio de baixo nível
- depuração com
gdb/valgrind - desenho de arquitetura
Sugestões:
- Compare com o bash:
- rode o mesmo comando no bash e no minishell e compare saída e status
- Ferramentas:
valgrind --leak-check=full ./minishellgdb ./minishell
Casos de teste úteis:
- Erros de sintaxe:
| lsecho hi >
- Aspas:
echo "a b"echo '$HOME'
- Expansões:
- `echo $?
echo "$USER"
- Pipes + redirs:
cat < infile | grep x > out
- Heredoc:
cat << EOF | wc -l
Dependendo do escopo do teu projeto, alguns itens normalmente não são exigidos:
&&e||;- wildcard
*(globbing) - subshell
( ... )
Se você implementou algum desses extras, documente aqui.
man bash,man readline,man execve,man fork,man pipe,man dup2,man waitpid,man signal- Materiais do 42 sobre minishell
- GitHub: @mr-body