Projeto Final: Mini-Shell
Vídeo da aula estará disponível em breve
Visão Geral do Projeto
Neste projeto final, vamos construir um mini-shell funcional em C que integra os principais conceitos do curso: processos (fork/exec/wait), I/O (redirecionamento, pipes), sinais e controle de jobs. Ao final, você terá um shell que executa comandos reais do sistema.
Nível 1 (básico):
✓ Prompt com diretório atual
✓ Executar comandos simples (ls, cat, grep, etc.)
✓ Comandos built-in: cd, exit, pwd
✓ Tratamento de argumentos
Nível 2 (intermediário):
✓ Redirecionamento de saída: comando > arquivo
✓ Redirecionamento de entrada: comando < arquivo
✓ Pipes: cmd1 | cmd2
Nível 3 (avançado):
✓ Tratamento de sinais (Ctrl+C, Ctrl+Z)
✓ Execução em background: comando &
✓ Job control básico: jobs, fg, bg
Conceitos do curso exercitados:
fork/exec/wait → Módulo 2 (Processos)
pipe/dup2 → Módulo 7 (E/S)
signals → Módulo 2 (Comunicação entre processos)
waitpid → Módulo 3 (Escalonamento/estados)
Passo 1: Loop Principal e Parsing
Todo shell tem o mesmo loop: ler comando, parsear, executar, repetir. Vamos começar com a estrutura básica.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>
#define MAX_LINE 1024 // tamanho máximo da linha
#define MAX_ARGS 64 // máximo de argumentos
#define MAX_JOBS 32 // máximo de jobs em background
/* ── Estrutura de um job em background ── */
typedef struct {
pid_t pid;
int id;
char cmd[MAX_LINE];
int stopped; // 1 se parado por Ctrl+Z
} Job;
Job jobs[MAX_JOBS];
int job_count = 0;
/* ── Parsear linha em array de tokens ── */
int parse_line(char* line, char** args) {
int argc = 0;
char* token = strtok(line, " \t\n");
while (token != NULL && argc < MAX_ARGS - 1) {
args[argc++] = token;
token = strtok(NULL, " \t\n");
}
args[argc] = NULL; // execvp exige NULL no final
return argc;
}
/* ── Exibir prompt ── */
void show_prompt() {
char cwd[MAX_LINE];
if (getcwd(cwd, sizeof(cwd)) != NULL) {
printf("\033[1;32mminishell\033[0m:\033[1;34m%s\033[0m$ ", cwd);
} else {
printf("minishell$ ");
}
fflush(stdout);
}
Passo 2: Executar Comandos com fork/exec/wait
O coração de qualquer shell Unix: o pai faz fork() para criar um filho, o filho faz exec() para substituir seu código pelo comando, e o pai faz wait() para esperar o filho terminar.
/* ── Comandos built-in ── */
int handle_builtin(char** args, int argc) {
if (argc == 0) return 1;
if (strcmp(args[0], "exit") == 0) {
printf("Saindo do minishell.\n");
exit(0);
}
if (strcmp(args[0], "cd") == 0) {
const char* dir = (argc > 1) ? args[1] : getenv("HOME");
if (chdir(dir) != 0)
perror("cd");
return 1; // comando tratado
}
if (strcmp(args[0], "pwd") == 0) {
char cwd[MAX_LINE];
if (getcwd(cwd, sizeof(cwd)))
printf("%s\n", cwd);
return 1;
}
if (strcmp(args[0], "jobs") == 0) {
for (int i = 0; i < job_count; i++) {
if (jobs[i].pid > 0) {
printf("[%d] %s %s\n", jobs[i].id,
jobs[i].stopped ? "Stopped" : "Running",
jobs[i].cmd);
}
}
return 1;
}
return 0; // não é built-in
}
/* ── Executar comando externo ── */
void execute_command(char** args, int background) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return;
}
if (pid == 0) {
/* ── FILHO ── */
// Restaurar handlers de sinais ao default
signal(SIGINT, SIG_DFL);
signal(SIGTSTP, SIG_DFL);
// Novo process group para job control
setpgid(0, 0);
execvp(args[0], args);
// Se execvp retorna, houve erro
fprintf(stderr, "minishell: comando não encontrado: %s\n", args[0]);
exit(127);
}
/* ── PAI ── */
if (background) {
// Registrar job em background
if (job_count < MAX_JOBS) {
jobs[job_count].pid = pid;
jobs[job_count].id = job_count + 1;
jobs[job_count].stopped = 0;
snprintf(jobs[job_count].cmd, MAX_LINE, "%s", args[0]);
printf("[%d] %d\n", jobs[job_count].id, pid);
job_count++;
}
} else {
// Foreground: esperar o filho terminar
int status;
waitpid(pid, &status, WUNTRACED);
if (WIFSTOPPED(status)) {
// Filho foi parado por Ctrl+Z
if (job_count < MAX_JOBS) {
jobs[job_count].pid = pid;
jobs[job_count].id = job_count + 1;
jobs[job_count].stopped = 1;
snprintf(jobs[job_count].cmd, MAX_LINE, "%s", args[0]);
printf("\n[%d] Stopped %s\n", jobs[job_count].id, args[0]);
job_count++;
}
}
}
}
execvp e não exec?
A família exec tem 6 variantes. execvp é a mais útil para shells: o v significa que argumentos vêm em um vetor (array), e o p significa que busca no PATH (não precisa passar caminho completo como /bin/ls).
Passo 3: Redirecionamento de E/S
Redirecionamento usa dup2() para substituir stdin (fd 0) ou stdout (fd 1) por um arquivo. A system call dup2(old_fd, new_fd) faz new_fd apontar para o mesmo arquivo que old_fd.
/* ── Processar redirecionamentos ── */
int setup_redirections(char** args, int argc) {
for (int i = 0; i < argc; i++) {
if (strcmp(args[i], ">") == 0 && i + 1 < argc) {
// Redirecionar stdout para arquivo (sobrescrever)
int fd = open(args[i + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { perror("open"); return -1; }
dup2(fd, STDOUT_FILENO); // stdout agora aponta para o arquivo
close(fd);
args[i] = NULL; // remover ">" e filename dos args
return 0;
}
if (strcmp(args[i], ">>") == 0 && i + 1 < argc) {
// Redirecionar stdout (append)
int fd = open(args[i + 1], O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd < 0) { perror("open"); return -1; }
dup2(fd, STDOUT_FILENO);
close(fd);
args[i] = NULL;
return 0;
}
if (strcmp(args[i], "<") == 0 && i + 1 < argc) {
// Redirecionar stdin de arquivo
int fd = open(args[i + 1], O_RDONLY);
if (fd < 0) { perror("open"); return -1; }
dup2(fd, STDIN_FILENO); // stdin agora lê do arquivo
close(fd);
args[i] = NULL;
return 0;
}
}
return 0;
}
/*
Como dup2 funciona:
Antes de dup2(fd, STDOUT_FILENO):
fd 0 (stdin) → terminal
fd 1 (stdout) → terminal ← saída normal
fd 3 → arquivo.txt
Depois de dup2(3, 1):
fd 0 (stdin) → terminal
fd 1 (stdout) → arquivo.txt ← agora stdout vai pro arquivo!
fd 3 → arquivo.txt
close(3) limpa o fd extra. fd 1 continua apontando pro arquivo.
printf() escreve no arquivo sem saber que mudou!
*/
Passo 4: Pipes
Pipes conectam a saída de um comando à entrada de outro. A system call pipe() cria dois file descriptors conectados: o que se escreve em um, sai pelo outro.
/* ── Executar pipeline de dois comandos ── */
void execute_pipe(char** cmd1, char** cmd2) {
int pipefd[2];
pipe(pipefd); // pipefd[0]=leitura, pipefd[1]=escrita
pid_t pid1 = fork();
if (pid1 == 0) {
/* ── Filho 1: cmd1 (escreve no pipe) ── */
close(pipefd[0]); // não precisa ler do pipe
dup2(pipefd[1], STDOUT_FILENO); // stdout → pipe
close(pipefd[1]);
signal(SIGINT, SIG_DFL);
execvp(cmd1[0], cmd1);
perror("execvp");
exit(1);
}
pid_t pid2 = fork();
if (pid2 == 0) {
/* ── Filho 2: cmd2 (lê do pipe) ── */
close(pipefd[1]); // não precisa escrever no pipe
dup2(pipefd[0], STDIN_FILENO); // stdin ← pipe
close(pipefd[0]);
signal(SIGINT, SIG_DFL);
execvp(cmd2[0], cmd2);
perror("execvp");
exit(1);
}
/* ── Pai: fechar pipe e esperar ambos ── */
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
}
/*
Diagrama do pipe:
┌────────┐ pipe ┌────────┐
│ cmd1 │──(stdout)──→│ cmd2 │
│ (ls) │ pipefd[1] │ (grep) │
└────────┘ pipefd[0] └────────┘
↑ ↓
escrita leitura
Exemplo: ls -la | grep ".c"
cmd1 = ["ls", "-la", NULL]
cmd2 = ["grep", ".c", NULL]
Regra crítica de pipes:
FECHAR todos os fds do pipe que não são usados!
Se o pai não fechar pipefd[0] e pipefd[1]:
cmd2 nunca receberá EOF e ficará travado forever.
*/
Passo 5: Sinais
O shell precisa tratar sinais para que Ctrl+C e Ctrl+Z não matem o próprio shell, mas sim o comando sendo executado.
/* ── Handler para SIGCHLD (filho terminou em background) ── */
void sigchld_handler(int sig) {
int status;
pid_t pid;
// Coletar todos os filhos terminados (sem bloquear)
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
// Encontrar e remover o job da lista
for (int i = 0; i < job_count; i++) {
if (jobs[i].pid == pid) {
printf("\n[%d] Done %s\n", jobs[i].id, jobs[i].cmd);
jobs[i].pid = 0; // marcar como terminado
break;
}
}
}
}
/* ── Configurar sinais no shell (chamado uma vez no main) ── */
void setup_signals() {
// Shell IGNORA Ctrl+C e Ctrl+Z
signal(SIGINT, SIG_IGN); // Ctrl+C: não mata o shell
signal(SIGTSTP, SIG_IGN); // Ctrl+Z: não para o shell
// Tratar SIGCHLD para coletar filhos background
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
}
/*
Fluxo de sinais:
Usuário digita Ctrl+C:
Kernel envia SIGINT a todos os processos no foreground group
Shell: ignora (SIG_IGN) → continua rodando
Filho: handler default (SIG_DFL) → termina
Usuário digita Ctrl+Z:
Kernel envia SIGTSTP ao foreground group
Shell: ignora
Filho: para (SIGTSTP) → waitpid retorna com WIFSTOPPED
Shell: registra como job stopped
Filho termina em background:
Kernel envia SIGCHLD ao pai (shell)
sigchld_handler: coleta com waitpid, imprime "[1] Done"
*/
Passo 6: Função main Completa
int main() {
char line[MAX_LINE];
char* args[MAX_ARGS];
setup_signals();
while (1) {
show_prompt();
// Ler linha (fgets é seguro: limita tamanho)
if (fgets(line, MAX_LINE, stdin) == NULL) {
printf("\n");
break; // Ctrl+D (EOF)
}
// Linha vazia? Próximo prompt
if (line[0] == '\n') continue;
// Guardar cópia para pipe parsing
char line_copy[MAX_LINE];
strncpy(line_copy, line, MAX_LINE);
// Verificar se tem pipe
char* pipe_pos = strchr(line_copy, '|');
if (pipe_pos) {
*pipe_pos = '\0';
char* cmd1_str = line_copy;
char* cmd2_str = pipe_pos + 1;
char* cmd1_args[MAX_ARGS];
char* cmd2_args[MAX_ARGS];
parse_line(cmd1_str, cmd1_args);
parse_line(cmd2_str, cmd2_args);
execute_pipe(cmd1_args, cmd2_args);
continue;
}
// Parsear argumentos
int argc = parse_line(line, args);
if (argc == 0) continue;
// Verificar background (&)
int background = 0;
if (strcmp(args[argc - 1], "&") == 0) {
background = 1;
args[argc - 1] = NULL;
argc--;
}
// Tentar built-in primeiro
if (handle_builtin(args, argc))
continue;
// Comando externo
pid_t pid = fork();
if (pid == 0) {
/* Filho */
signal(SIGINT, SIG_DFL);
signal(SIGTSTP, SIG_DFL);
setup_redirections(args, argc);
execvp(args[0], args);
fprintf(stderr, "minishell: %s: comando não encontrado\n", args[0]);
exit(127);
} else if (pid > 0) {
/* Pai */
if (background) {
if (job_count < MAX_JOBS) {
jobs[job_count].pid = pid;
jobs[job_count].id = job_count + 1;
jobs[job_count].stopped = 0;
snprintf(jobs[job_count].cmd, MAX_LINE, "%s", args[0]);
printf("[%d] %d\n", jobs[job_count].id, pid);
job_count++;
}
} else {
int status;
waitpid(pid, &status, WUNTRACED);
if (WIFSTOPPED(status) && job_count < MAX_JOBS) {
jobs[job_count].pid = pid;
jobs[job_count].id = job_count + 1;
jobs[job_count].stopped = 1;
snprintf(jobs[job_count].cmd, MAX_LINE, "%s", args[0]);
printf("\n[%d] Stopped %s\n", jobs[job_count].id, args[0]);
job_count++;
}
}
}
}
return 0;
}
Compilar e Testar
# Compilar
$ gcc -Wall -Wextra -o minishell minishell.c
# Executar
$ ./minishell
# Testes nível 1 (básico):
minishell:/home/marco$ ls -la
minishell:/home/marco$ pwd
minishell:/home/marco$ cd /tmp
minishell:/tmp$ echo "hello world"
minishell:/tmp$ cd
minishell:/home/marco$ exit
# Testes nível 2 (redirecionamento e pipe):
minishell:~$ ls -la > listagem.txt
minishell:~$ cat listagem.txt
minishell:~$ wc -l < listagem.txt
minishell:~$ ls -la >> listagem.txt
minishell:~$ ls -la | grep ".c"
minishell:~$ cat /etc/passwd | grep root
# Testes nível 3 (sinais e background):
minishell:~$ sleep 100 &
[1] 12345
minishell:~$ jobs
[1] Running sleep
minishell:~$ sleep 50 # depois Ctrl+C → mata o sleep
^C
minishell:~$ sleep 50 # depois Ctrl+Z → para o sleep
^Z
[2] Stopped sleep
minishell:~$ jobs
[1] Running sleep
[2] Stopped sleep
No harness.os
O mini-shell que construímos é uma versão simplificada do que o shell real faz quando você executa comandos do harness. O pipeline de operações é idêntico:
Quando você executa um agente no terminal:
$ claude "log a decision about using React"
O shell real (bash/zsh) faz exatamente o que nosso mini-shell faz:
1. fork() → cria processo filho
2. exec() → substitui por binário do claude
3. Claude inicia → chama MCP tools (log_decision)
4. MCP tools → queries no Neon Postgres
5. Claude termina → exit()
6. Shell faz wait() → coleta status do filho
7. Shell exibe prompt novamente
Com pipe:
$ cat rules.json | claude "review these rules"
↑
pipe() + dup2()
stdout do cat → stdin do claude
O mini-shell que construímos implementa as mesmas system calls
que o bash usa para executar qualquer programa no seu sistema.
Exercícios de Extensão
- Pipes múltiplos: Estenda o mini-shell para suportar
cmd1 | cmd2 | cmd3(pipeline de N comandos). Dica: crie N-1 pipes e N filhos em um loop. - Histórico: Implemente um comando
historyque mostra os últimos 20 comandos digitados. Armazene em um array circular. Bônus: salve em~/.minishell_historyao sair. - fg e bg: Implemente os comandos
fg %N(trazer job N para foreground) ebg %N(continuar job parado em background). Usekill(pid, SIGCONT)para continuar etcsetpgrp()para controle de terminal. - Variáveis de ambiente: Implemente
export VAR=valore$VARexpansion. Usesetenv()e substitua tokens que começam com$pelo valor correspondente viagetenv().
Resumo
Verifique seu entendimento
Por que o processo filho deve fechar as pontas do pipe que não utiliza?
- Um shell é um loop de: ler comando, parsear, fork, exec (no filho), wait (no pai)
- Comandos built-in (cd, exit, pwd) executam no processo do shell, sem fork
- Redirecionamento usa
dup2()para substituir stdin/stdout por file descriptors de arquivos - Pipes usam
pipe()para criar um canal edup2()para conectar stdout do produtor ao stdin do consumidor - O shell ignora SIGINT/SIGTSTP para não ser morto por Ctrl+C/Ctrl+Z; filhos usam handlers default
- Construir um shell exercita processos, I/O, sinais e gerência de recursos — todos os conceitos do curso