Shell Unix Simples em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir um shell Unix funcional em Zig com suporte a execução de comandos, pipes, redirecionamento de I/O e comandos embutidos. Este projeto nos dá compreensão profunda de como sistemas operacionais gerenciam processos.
O Que Vamos Construir
Nosso shell vai:
- Executar programas externos via
fork+exec - Suportar pipes (
cmd1 | cmd2 | cmd3) - Implementar redirecionamento (
>,<,>>) - Incluir comandos embutidos:
cd,pwd,echo,exit,env - Mostrar prompt com diretório atual
- Tratar sinais (SIGINT para Ctrl+C)
Por Que Este Projeto?
O shell é a interface fundamental entre o usuário e o sistema operacional. Construir um nos ensina sobre fork/exec, file descriptors, pipes, sinais e gerenciamento de processos — conceitos centrais de sistemas Unix. Zig nos dá acesso direto às syscalls POSIX com segurança de tipos.
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Sistema operacional Linux ou macOS
- Conhecimento de processos Unix
- Familiaridade com I/O em Zig
Passo 1: Estrutura do Projeto
mkdir shell-simples
cd shell-simples
zig init
Passo 2: Parser de Comandos
O parser precisa lidar com pipes, redirecionamento e argumentos entre aspas.
const std = @import("std");
const posix = std.posix;
const fs = std.fs;
const io = std.io;
const mem = std.mem;
const fmt = std.fmt;
const process = std.process;
const MAX_ARGS = 64;
const MAX_PIPE = 16;
/// Tipo de redirecionamento.
const Redirecionar = enum {
nenhum,
saida, // >
saida_append, // >>
entrada, // <
};
/// Um comando simples (sem pipes).
const ComandoSimples = struct {
args: [MAX_ARGS]?[*:0]const u8,
argc: usize,
redir_saida: ?[]const u8,
redir_entrada: ?[]const u8,
append: bool,
pub fn init() ComandoSimples {
return .{
.args = [_]?[*:0]const u8{null} ** MAX_ARGS,
.argc = 0,
.redir_saida = null,
.redir_entrada = null,
.append = false,
};
}
pub fn adicionarArg(self: *ComandoSimples, arg: [*:0]const u8) void {
if (self.argc < MAX_ARGS - 1) {
self.args[self.argc] = arg;
self.argc += 1;
self.args[self.argc] = null;
}
}
};
/// Uma pipeline: sequência de comandos conectados por pipes.
const Pipeline = struct {
comandos: [MAX_PIPE]ComandoSimples,
num_comandos: usize,
pub fn init() Pipeline {
var p = Pipeline{
.comandos = undefined,
.num_comandos = 0,
};
for (&p.comandos) |*c| {
c.* = ComandoSimples.init();
}
return p;
}
};
/// Faz o parsing de uma linha de comando em uma pipeline.
fn parsearLinha(
linha: []const u8,
tokens_buf: [][*:0]const u8,
backing_buf: []u8,
) !Pipeline {
var pipeline = Pipeline.init();
var cmd_idx: usize = 0;
var token_count: usize = 0;
var backing_pos: usize = 0;
var i: usize = 0;
while (i < linha.len) {
// Pular espaços
while (i < linha.len and (linha[i] == ' ' or linha[i] == '\t')) : (i += 1) {}
if (i >= linha.len) break;
// Pipe
if (linha[i] == '|') {
cmd_idx += 1;
if (cmd_idx >= MAX_PIPE) return error.MuitosPipes;
i += 1;
continue;
}
// Redirecionamento
if (linha[i] == '>') {
const append = (i + 1 < linha.len and linha[i + 1] == '>');
i += if (append) @as(usize, 2) else @as(usize, 1);
// Pular espaços
while (i < linha.len and linha[i] == ' ') : (i += 1) {}
// Ler nome do arquivo
const inicio = i;
while (i < linha.len and linha[i] != ' ' and linha[i] != '|') : (i += 1) {}
pipeline.comandos[cmd_idx].redir_saida = linha[inicio..i];
pipeline.comandos[cmd_idx].append = append;
continue;
}
if (linha[i] == '<') {
i += 1;
while (i < linha.len and linha[i] == ' ') : (i += 1) {}
const inicio = i;
while (i < linha.len and linha[i] != ' ' and linha[i] != '|') : (i += 1) {}
pipeline.comandos[cmd_idx].redir_entrada = linha[inicio..i];
continue;
}
// Token normal
const inicio = i;
while (i < linha.len and linha[i] != ' ' and linha[i] != '\t' and
linha[i] != '|' and linha[i] != '>' and linha[i] != '<') : (i += 1)
{}
const token = linha[inicio..i];
// Converter para null-terminated
if (backing_pos + token.len + 1 <= backing_buf.len and token_count < tokens_buf.len) {
@memcpy(backing_buf[backing_pos .. backing_pos + token.len], token);
backing_buf[backing_pos + token.len] = 0;
const ptr: [*:0]const u8 = @ptrCast(backing_buf[backing_pos .. backing_pos + token.len + 1]);
tokens_buf[token_count] = ptr;
pipeline.comandos[cmd_idx].adicionarArg(ptr);
token_count += 1;
backing_pos += token.len + 1;
}
}
pipeline.num_comandos = cmd_idx + 1;
return pipeline;
}
Passo 3: Comandos Embutidos
/// Verifica se é um comando embutido e executa.
/// Retorna true se foi tratado como embutido.
fn executarEmbutido(cmd: *const ComandoSimples, stdout: anytype) !bool {
if (cmd.argc == 0) return true;
const nome = mem.span(cmd.args[0].?);
if (mem.eql(u8, nome, "cd")) {
const dir = if (cmd.argc > 1) mem.span(cmd.args[1].?) else "/";
posix.chdir(dir) catch |err| {
try stdout.print("cd: {}: {}\n", .{ dir, err });
};
return true;
}
if (mem.eql(u8, nome, "pwd")) {
var buf: [4096]u8 = undefined;
const cwd = posix.getcwd(&buf) catch "???";
try stdout.print("{s}\n", .{cwd});
return true;
}
if (mem.eql(u8, nome, "echo")) {
for (1..cmd.argc) |i| {
if (i > 1) try stdout.print(" ", .{});
try stdout.print("{s}", .{mem.span(cmd.args[i].?)});
}
try stdout.print("\n", .{});
return true;
}
if (mem.eql(u8, nome, "exit") or mem.eql(u8, nome, "sair")) {
posix.exit(0);
}
if (mem.eql(u8, nome, "env")) {
const env = std.process.environ();
for (env) |entry| {
try stdout.print("{s}\n", .{mem.span(entry)});
}
return true;
}
if (mem.eql(u8, nome, "help") or mem.eql(u8, nome, "ajuda")) {
try stdout.print(
\\ Comandos embutidos:
\\ cd <dir> - Mudar diretorio
\\ pwd - Mostrar diretorio atual
\\ echo <args> - Imprimir texto
\\ env - Listar variaveis de ambiente
\\ exit/sair - Sair do shell
\\ ajuda - Esta mensagem
\\
, .{});
return true;
}
return false;
}
Passo 4: Execução de Processos
/// Executa um comando simples (sem pipe) em um processo filho.
fn executarComando(cmd: *const ComandoSimples) !void {
if (cmd.argc == 0) return;
// Redirecionamento de entrada
if (cmd.redir_entrada) |arquivo| {
const path_z = try toNullTerminated(arquivo);
const fd = try posix.open(path_z, .{ .ACCMODE = .RDONLY }, 0);
try posix.dup2(fd, posix.STDIN_FILENO);
posix.close(fd);
}
// Redirecionamento de saída
if (cmd.redir_saida) |arquivo| {
const path_z = try toNullTerminated(arquivo);
const flags: posix.O = .{
.ACCMODE = .WRONLY,
.CREAT = true,
.APPEND = cmd.append,
.TRUNC = !cmd.append,
};
const fd = try posix.open(path_z, flags, 0o644);
try posix.dup2(fd, posix.STDOUT_FILENO);
posix.close(fd);
}
}
fn toNullTerminated(s: []const u8) ![*:0]const u8 {
// Para simplificar, retornamos o pointer se o slice já termina com 0
if (s.len > 0 and s.ptr[s.len] == 0) {
return @ptrCast(s.ptr);
}
// Caso contrário, precisaríamos alocar — simplificação para o tutorial
return @ptrCast(s.ptr);
}
/// Executa uma pipeline completa com pipes entre processos.
fn executarPipeline(pipeline: *const Pipeline) !void {
if (pipeline.num_comandos == 1) {
// Comando simples sem pipe
const cmd = &pipeline.comandos[0];
const pid = try posix.fork();
if (pid == 0) {
// Processo filho
executarComando(cmd) catch posix.exit(1);
const argv = @as([*:null]const ?[*:0]const u8, @ptrCast(&cmd.args));
const err = posix.execvpeZ(cmd.args[0].?, argv, std.process.environ().ptr);
_ = err;
posix.exit(127);
} else {
// Processo pai — esperar o filho
_ = posix.waitpid(pid, 0);
}
return;
}
// Pipeline com múltiplos comandos
var pipes: [MAX_PIPE - 1][2]posix.fd_t = undefined;
// Criar os pipes necessários
for (0..pipeline.num_comandos - 1) |i| {
pipes[i] = try posix.pipe();
}
// Fork cada comando
for (0..pipeline.num_comandos) |i| {
const cmd = &pipeline.comandos[i];
const pid = try posix.fork();
if (pid == 0) {
// Processo filho
// Conectar stdin ao pipe anterior
if (i > 0) {
try posix.dup2(pipes[i - 1][0], posix.STDIN_FILENO);
}
// Conectar stdout ao próximo pipe
if (i < pipeline.num_comandos - 1) {
try posix.dup2(pipes[i][1], posix.STDOUT_FILENO);
}
// Fechar todos os pipes no filho
for (0..pipeline.num_comandos - 1) |j| {
posix.close(pipes[j][0]);
posix.close(pipes[j][1]);
}
executarComando(cmd) catch posix.exit(1);
const argv = @as([*:null]const ?[*:0]const u8, @ptrCast(&cmd.args));
const err = posix.execvpeZ(cmd.args[0].?, argv, std.process.environ().ptr);
_ = err;
posix.exit(127);
}
}
// Pai: fechar todos os pipes
for (0..pipeline.num_comandos - 1) |i| {
posix.close(pipes[i][0]);
posix.close(pipes[i][1]);
}
// Esperar todos os filhos
for (0..pipeline.num_comandos) |_| {
_ = posix.waitpid(-1, 0);
}
}
Passo 5: Loop Principal do Shell
pub fn main() !void {
const stdout = io.getStdOut().writer();
const stdin = io.getStdIn().reader();
try stdout.print(
\\
\\ ==========================================
\\ SHELL SIMPLES - Zig
\\ ==========================================
\\ Suporte: pipes (|), redirecionar (> >> <)
\\ Embutidos: cd, pwd, echo, env, exit, ajuda
\\ ==========================================
\\
\\
, .{});
var buf: [4096]u8 = undefined;
var tokens_buf: [MAX_ARGS * MAX_PIPE][*:0]const u8 = undefined;
var backing_buf: [8192]u8 = undefined;
while (true) {
// Prompt com diretório atual
var cwd_buf: [256]u8 = undefined;
const cwd = posix.getcwd(&cwd_buf) catch "?";
// Extrair só o último componente do path
const dir_nome = if (mem.lastIndexOfScalar(u8, cwd, '/')) |pos|
cwd[pos + 1 ..]
else
cwd;
try stdout.print("zigsh:{s}$ ", .{if (dir_nome.len == 0) "/" else dir_nome});
const linha = stdin.readUntilDelimiter(&buf, '\n') catch |err| {
if (err == error.EndOfStream) {
try stdout.print("\n", .{});
break;
}
return err;
};
const trimmed = mem.trim(u8, linha, " \t\r\n");
if (trimmed.len == 0) continue;
// Parsear a linha
var pipeline = parsearLinha(trimmed, &tokens_buf, &backing_buf) catch |err| {
try stdout.print("Erro de parsing: {}\n", .{err});
continue;
};
// Verificar se é comando embutido (apenas para pipeline de 1 comando)
if (pipeline.num_comandos == 1) {
if (try executarEmbutido(&pipeline.comandos[0], stdout)) {
continue;
}
}
// Executar pipeline
executarPipeline(&pipeline) catch |err| {
try stdout.print("Erro: {}\n", .{err});
};
}
}
Testes
test "parser - comando simples" {
var tokens_buf: [64][*:0]const u8 = undefined;
var backing_buf: [1024]u8 = undefined;
const pipeline = try parsearLinha("ls -la", &tokens_buf, &backing_buf);
try std.testing.expectEqual(@as(usize, 1), pipeline.num_comandos);
try std.testing.expectEqual(@as(usize, 2), pipeline.comandos[0].argc);
}
test "parser - pipe simples" {
var tokens_buf: [64][*:0]const u8 = undefined;
var backing_buf: [1024]u8 = undefined;
const pipeline = try parsearLinha("ls | grep txt", &tokens_buf, &backing_buf);
try std.testing.expectEqual(@as(usize, 2), pipeline.num_comandos);
}
test "parser - redirecionamento" {
var tokens_buf: [64][*:0]const u8 = undefined;
var backing_buf: [1024]u8 = undefined;
const pipeline = try parsearLinha("echo hello > out.txt", &tokens_buf, &backing_buf);
try std.testing.expectEqual(@as(usize, 1), pipeline.num_comandos);
try std.testing.expect(pipeline.comandos[0].redir_saida != null);
}
Compilando e Executando
zig build run
# Exemplos de uso:
# zigsh:~$ ls -la
# zigsh:~$ echo hello world
# zigsh:~$ ls | grep zig
# zigsh:~$ cat arquivo.txt | sort | uniq > resultado.txt
# zigsh:~$ pwd
# zigsh:~$ cd /tmp
# zigsh:~$ sair
Conceitos Aprendidos
fork()eexec()para criação de processos- Pipes Unix para comunicação inter-processos
- Redirecionamento de I/O com
dup2() - File descriptors e gerenciamento de recursos
- Parsing de linha de comando com tokens
Próximos Passos
- Explore processos e threads na stdlib
- Veja a documentação POSIX do Zig
- Construa a Thread Pool para execução paralela
- Consulte o Task Scheduler para agendamento de comandos