Ferramentas de linha de comando (CLI) são essenciais no arsenal de qualquer desenvolvedor. Seja um utilitário de build, um gerador de código, ou uma ferramenta de automação, saber criar CLIs eficientes é uma habilidade valiosa.
Neste tutorial, vamos construir uma CLI completa em Zig — do zero até uma ferramenta profissional com parsing de argumentos, subcomandos, saída colorida, e mais.
O que você vai aprender:
- Ler e parsear argumentos da linha de comando
- Criar subcomandos (como
git commit,git push)- Adicionar cores e formatação ao terminal
- Ler input do usuário
- Criar uma CLI completa e funcional
Pré-requisitos
Antes de começar, você precisa ter:
- Zig instalado — veja Como Instalar o Zig se ainda não tiver
- Conhecimento básico de Zig — variáveis, funções, e structs
Estrutura do Projeto
Vamos criar uma CLI chamada taskman — um gerenciador simples de tarefas para a linha de comando.
taskman/
├── build.zig
├── build.zig.zon
└── src/
└── main.zig
Passo 1: Criar o Projeto
# Criar diretório do projeto
mkdir taskman
cd taskman
# Inicializar projeto Zig
zig init
Isso cria a estrutura básica com build.zig e src/main.zig.
Passo 2: Ler Argumentos da Linha de Comando
O Zig acessa argumentos através de std.process.args().
// src/main.zig
const std = @import("std");
pub fn main() !void {
// Allocator para trabalhar com strings
var gpa = std.heap.GeneralPurposeAllocator(.){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Coletar argumentos
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
// args[0] é o caminho do executável
// args[1..] são os argumentos passados
std.debug.print("Programa: {s}\n", .{args[0]});
std.debug.print("Argumentos ({d}):\n", .{args.len - 1});
for (args[1..], 1..) |arg, i| {
std.debug.print(" [{d}] {s}\n", .{ i, arg });
}
}
Teste:
zig build run -- arg1 arg2 arg3
Saída:
Programa: /caminho/para/taskman
Argumentos (3):
[1] arg1
[2] arg2
[3] arg3
Passo 3: Parsing Básico de Argumentos
Vamos criar uma estrutura para representar nossos comandos:
// src/main.zig
const std = @import("std");
const Command = enum {
add,
list,
done,
help,
unknown,
pub fn fromString(s: []const u8) Command {
if (std.mem.eql(u8, s, "add")) return .add;
if (std.mem.eql(u8, s, "list")) return .list;
if (std.mem.eql(u8, s, "done")) return .done;
if (std.mem.eql(u8, s, "help")) return .help;
return .unknown;
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len < 2) {
try printHelp();
return;
}
const command = Command.fromString(args[1]);
switch (command) {
.add => try cmdAdd(args[2..]),
.list => try cmdList(),
.done => try cmdDone(args[2..]),
.help => try printHelp(),
.unknown => {
std.debug.print("Comando desconhecido: {s}\n", .{args[1]});
try printHelp();
},
}
}
fn printHelp() !void {
const stdout = std.io.getStdOut().writer();
try stdout.writeAll(
\\Taskman - Gerenciador de Tarefas
\\
\\Uso: taskman <comando> [args]
\\
\\Comandos:
\\ add <descrição> Adicionar nova tarefa
\\ list Listar todas as tarefas
\\ done <número> Marcar tarefa como concluída
\\ help Mostrar esta ajuda
\\
);
}
fn cmdAdd(args: []const []const u8) !void {
if (args.len == 0) {
std.debug.print("Erro: Descrição da tarefa necessária\n");
std.debug.print("Uso: taskman add <descrição>\n");
return;
}
// Juntar todos os argumentos em uma string
// Por simplicidade, vamos apenas imprimir
std.debug.print("Adicionando tarefa: ", .{});
for (args) |arg| {
std.debug.print("{s} ", .{arg});
}
std.debug.print("\n", .{});
}
fn cmdList() !void {
std.debug.print("Lista de tarefas:\n", .{});
std.debug.print(" 1. [ ] Implementar CLI em Zig\n", .{});
std.debug.print(" 2. [ ] Escrever documentação\n", .{});
std.debug.print(" 3. [x] Configurar projeto\n", .{});
}
fn cmdDone(args: []const []const u8) !void {
if (args.len == 0) {
std.debug.print("Erro: Número da tarefa necessário\n");
return;
}
std.debug.print("Marcando tarefa {s} como concluída\n", .{args[0]});
}
Teste os comandos:
zig build run -- help
zig build run -- add "Implementar parsing de flags"
zig build run -- list
zig build run -- done 1
Passo 4: Adicionar Cores ao Terminal
Vamos melhorar a saída com cores ANSI:
// src/main.zig
const std = @import("std");
// Códigos ANSI para cores
const Color = struct {
pub const reset = "\x1b[0m";
pub const red = "\x1b[31m";
pub const green = "\x1b[32m";
pub const yellow = "\x1b[33m";
pub const blue = "\x1b[34m";
pub const magenta = "\x1b[35m";
pub const cyan = "\x1b[36m";
pub const bold = "\x1b[1m";
};
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// Exemplo de saída colorida
try stdout.print("{s}Taskman{s} - {s}Gerenciador de Tarefas{s}\n\n", .{
Color.bold, Color.reset,
Color.cyan, Color.reset,
});
try stdout.print("{s}Comandos disponíveis:{s}\n", .{ Color.yellow, Color.reset });
try stdout.print(" {s}add{s} Adicionar tarefa\n", .{ Color.green, Color.reset });
try stdout.print(" {s}list{s} Listar tarefas\n", .{ Color.blue, Color.reset });
try stdout.print(" {s}done{s} Marcar como concluída\n", .{ Color.magenta, Color.reset });
}
Passo 5: Parsing de Flags (Argumentos Opcionais)
Vamos adicionar suporte a flags como --priority high:
// src/main.zig
const std = @import("std");
const Task = struct {
description: []const u8,
priority: Priority,
done: bool,
const Priority = enum {
low,
medium,
high,
pub fn fromString(s: []const u8) Priority {
if (std.mem.eql(u8, s, "low") or std.mem.eql(u8, s, "baixa")) return .low;
if (std.mem.eql(u8, s, "high") or std.mem.eql(u8, s, "alta")) return .high;
return .medium;
}
pub fn color(self: Priority) []const u8 {
return switch (self) {
.low => "\x1b[32m", // verde
.medium => "\x1b[33m", // amarelo
.high => "\x1b[31m", // vermelho
};
}
};
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len < 2) {
try printHelp();
return;
}
// Parse flags e argumentos posicionais
var flags = std.StringHashMap([]const u8).init(allocator);
defer flags.deinit();
var positional = std.ArrayList([]const u8).init(allocator);
defer positional.deinit();
var i: usize = 2;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.startsWith(u8, arg, "--")) {
// Flag longa: --name value
const name = arg[2..];
if (i + 1 < args.len) {
try flags.put(name, args[i + 1]);
i += 1;
}
} else if (std.mem.startsWith(u8, arg, "-") and arg.len > 1) {
// Flag curta: -p high
const name = arg[1..];
if (i + 1 < args.len) {
try flags.put(name, args[i + 1]);
i += 1;
}
} else {
try positional.append(arg);
}
}
// Executar comando
const command = args[1];
if (std.mem.eql(u8, command, "add")) {
try cmdAdd(positional.items, flags);
} else {
std.debug.print("Comando desconhecido: {s}\n", .{command});
}
}
fn cmdAdd(args: [][]const u8, flags: std.StringHashMap([]const u8)) !void {
if (args.len == 0) {
std.debug.print("Erro: Descrição necessária\n");
return;
}
// Obter prioridade da flag ou usar default
const priority_str = flags.get("priority") orelse flags.get("p") orelse "medium";
const priority = Task.Priority.fromString(priority_str);
// Concatenar descrição
std.debug.print("{s}✓{s} Tarefa adicionada:\n", .{ "\x1b[32m", "\x1b[0m" });
std.debug.print(" Descrição: ", .{});
for (args) |arg| {
std.debug.print("{s} ", .{arg});
}
std.debug.print("\n", .{});
std.debug.print(" Prioridade: {s}{s}{s}\n", .{
priority.color(), @tagName(priority), "\x1b[0m",
});
}
fn printHelp() !void {
const stdout = std.io.getStdOut().writer();
try stdout.writeAll(
\\Uso: taskman <comando> [args] [flags]
\\
\\Flags:
\\ -p, --priority <low|medium|high> Prioridade da tarefa
\\
);
}
Teste com flags:
zig build run -- add "Implementar feature X" --priority high
zig build run -- add "Bugfix menu" -p low
Passo 6: Ler Input do Usuário
Vamos adicionar interatividade:
// src/main.zig
fn readLine(allocator: std.mem.Allocator, prompt: []const u8) ![]u8 {
const stdout = std.io.getStdOut().writer();
const stdin = std.io.getStdIn().reader();
try stdout.print("{s}", .{prompt});
var buffer: [1024]u8 = undefined;
const line = try stdin.readUntilDelimiterOrEof(&buffer, '\n');
if (line) |l| {
// Remover newline e alocar
const trimmed = std.mem.trimRight(u8, l, "\r\n");
return try allocator.dupe(u8, trimmed);
}
return try allocator.dupe(u8, "");
}
fn cmdInteractive(allocator: std.mem.Allocator) !void {
const stdout = std.io.getStdOut().writer();
try stdout.writeAll("\x1b[1mModo Interativo\x1b[0m\n\n");
const desc = try readLine(allocator, "Descrição: ");
defer allocator.free(desc);
const priority_str = try readLine(allocator, "Prioridade (low/medium/high): ");
defer allocator.free(priority_str);
std.debug.print("\n{s}Tarefa criada:{s}\n", .{ "\x1b[32m", "\x1b[0m" });
std.debug.print(" {s}\n", .{desc});
std.debug.print(" Prioridade: {s}\n", .{priority_str});
}
Passo 7: Persistência (Salvar em Arquivo)
Vamos adicionar persistência simples em arquivo:
// src/main.zig
const std = @import("std");
const Task = struct {
id: u32,
description: []const u8,
priority: []const u8,
done: bool,
created_at: i64,
};
const TaskStore = struct {
tasks: std.ArrayList(Task),
next_id: u32,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) TaskStore {
return .{
.tasks = std.ArrayList(Task).init(allocator),
.next_id = 1,
.allocator = allocator,
};
}
pub fn deinit(self: *TaskStore) void {
for (self.tasks.items) |task| {
self.allocator.free(task.description);
self.allocator.free(task.priority);
}
self.tasks.deinit();
}
pub fn add(self: *TaskStore, description: []const u8, priority: []const u8) !void {
const task = Task{
.id = self.next_id,
.description = try self.allocator.dupe(u8, description),
.priority = try self.allocator.dupe(u8, priority),
.done = false,
.created_at = std.time.timestamp(),
};
self.next_id += 1;
try self.tasks.append(task);
}
pub fn save(self: *const TaskStore, filename: []const u8) !void {
const file = try std.fs.cwd().createFile(filename, .{});
defer file.close();
const writer = file.writer();
for (self.tasks.items) |task| {
try writer.print("{d}|{s}|{s}|{}|{d}\n", .{
task.id,
task.description,
task.priority,
task.done,
task.created_at,
});
}
}
pub fn load(self: *TaskStore, filename: []const u8) !void {
const file = std.fs.cwd().openFile(filename, .{}) catch |err| switch (err) {
error.FileNotFound => return,
else => return err,
};
defer file.close();
const reader = file.reader();
var buffer: [1024]u8 = undefined;
while (try reader.readUntilDelimiterOrEof(&buffer, '\n')) |line| {
// Parse: id|desc|priority|done|timestamp
var iter = std.mem.split(u8, line, "|");
const id_str = iter.next() orelse continue;
const desc = iter.next() orelse continue;
const priority = iter.next() orelse continue;
const done_str = iter.next() orelse continue;
const timestamp_str = iter.next() orelse continue;
const id = try std.fmt.parseInt(u32, id_str, 10);
const done = std.mem.eql(u8, done_str, "true");
const timestamp = try std.fmt.parseInt(i64, timestamp_str, 10);
const task = Task{
.id = id,
.description = try self.allocator.dupe(u8, desc),
.priority = try self.allocator.dupe(u8, priority),
.done = done,
.created_at = timestamp,
};
try self.tasks.append(task);
if (id >= self.next_id) {
self.next_id = id + 1;
}
}
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.){};
defer {
const status = gpa.deinit();
if (status == .leak) @panic("Memory leak!");
}
const allocator = gpa.allocator();
var store = TaskStore.init(allocator);
defer store.deinit();
// Carregar tarefas existentes
try store.load("tasks.txt");
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len < 2) {
try printHelp();
return;
}
const command = args[1];
if (std.mem.eql(u8, command, "add")) {
if (args.len < 3) {
std.debug.print("Erro: Descrição necessária\n");
return;
}
try store.add(args[2], "medium");
try store.save("tasks.txt");
std.debug.print("✓ Tarefa adicionada (ID: {d})\n", .{store.next_id - 1});
} else if (std.mem.eql(u8, command, "list")) {
try cmdList(&store);
} else {
try printHelp();
}
}
fn cmdList(store: *const TaskStore) !void {
const stdout = std.io.getStdOut().writer();
if (store.tasks.items.len == 0) {
try stdout.writeAll("Nenhuma tarefa encontrada.\n");
return;
}
try stdout.writeAll("\x1b[1mSuas Tarefas:\x1b[0m\n\n");
for (store.tasks.items) |task| {
const status = if (task.done) "\x1b[32m✓\x1b[0m" else "\x1b[31m○\x1b[0m";
const priority_color = switch (task.priority[0]) {
'h' => "\x1b[31m", // high = red
'l' => "\x1b[32m", // low = green
else => "\x1b[33m", // medium = yellow
};
try stdout.print("{s} [{d}] {s} {s}({s}){s}\n", .{
status,
task.id,
task.description,
priority_color,
task.priority,
"\x1b[0m",
});
}
}
fn printHelp() !void {
const stdout = std.io.getStdOut().writer();
try stdout.writeAll(
\\Taskman - Gerenciador de Tarefas
\\
\\Uso: taskman <comando> [args]
\\
\\Comandos:
\\ add <desc> Adicionar tarefa
\\ list Listar tarefas
\\
);
}
Passo 8: Build de Release
Compile a versão final otimizada:
# Build de release (otimizado)
zig build -Doptimize=ReleaseSafe
# Ou para tamanho mínimo
zig build -Doptimize=ReleaseSmall
# Executar
./zig-out/bin/taskman add "Minha primeira tarefa"
./zig-out/bin/taskman list
Versão Final Completa
Aqui está a CLI completa com todas as features:
// src/main.zig
const std = @import("std");
// Cores ANSI
const ANSI = struct {
const reset = "\x1b[0m";
const bold = "\x1b[1m";
const red = "\x1b[31m";
const green = "\x1b[32m";
const yellow = "\x1b[33m";
const blue = "\x1b[34m";
const cyan = "\x1b[36m";
};
const Task = struct {
id: u32,
description: []const u8,
priority: Priority,
done: bool,
created_at: i64,
const Priority = enum {
low,
medium,
high,
pub fn fromString(s: []const u8) Priority {
if (std.mem.eql(u8, s, "low") or std.mem.eql(u8, s, "baixa")) return .low;
if (std.mem.eql(u8, s, "high") or std.mem.eql(u8, s, "alta")) return .high;
return .medium;
}
pub fn color(self: Priority) []const u8 {
return switch (self) {
.low => ANSI.green,
.medium => ANSI.yellow,
.high => ANSI.red,
};
}
pub fn label(self: Priority) []const u8 {
return switch (self) {
.low => "baixa",
.medium => "média",
.high => "alta",
};
}
};
};
const TaskStore = struct {
tasks: std.ArrayList(Task),
next_id: u32,
allocator: std.mem.Allocator,
const DATA_FILE = "tasks.dat";
pub fn init(allocator: std.mem.Allocator) TaskStore {
return .{
.tasks = std.ArrayList(Task).init(allocator),
.next_id = 1,
.allocator = allocator,
};
}
pub fn deinit(self: *TaskStore) void {
for (self.tasks.items) |task| {
self.allocator.free(task.description);
}
self.tasks.deinit();
}
pub fn add(self: *TaskStore, description: []const u8, priority: Task.Priority) !void {
const task = Task{
.id = self.next_id,
.description = try self.allocator.dupe(u8, description),
.priority = priority,
.done = false,
.created_at = std.time.timestamp(),
};
self.next_id += 1;
try self.tasks.append(task);
}
pub fn done(self: *TaskStore, id: u32) bool {
for (self.tasks.items) |*task| {
if (task.id == id) {
task.done = true;
return true;
}
}
return false;
}
pub fn remove(self: *TaskStore, id: u32) bool {
for (self.tasks.items, 0..) |task, i| {
if (task.id == id) {
self.allocator.free(task.description);
_ = self.tasks.orderedRemove(i);
return true;
}
}
return false;
}
pub fn save(self: *const TaskStore) !void {
const file = try std.fs.cwd().createFile(DATA_FILE, .{});
defer file.close();
const writer = file.writer();
for (self.tasks.items) |task| {
try writer.print("{d}\n{s}\n{d}\n{}\n{d}\n", .{
task.id,
task.description,
@intFromEnum(task.priority),
task.done,
task.created_at,
});
}
}
pub fn load(self: *TaskStore) !void {
const file = std.fs.cwd().openFile(DATA_FILE, .{}) catch return;
defer file.close();
const reader = file.reader();
var buf: [4096]u8 = undefined;
while (true) {
const id_line = reader.readUntilDelimiterOrEof(&buf, '\n') catch break;
if (id_line == null) break;
const desc_line = (reader.readUntilDelimiterOrEof(&buf, '\n') catch break).?;
const priority_line = (reader.readUntilDelimiterOrEof(&buf, '\n') catch break).?;
const done_line = (reader.readUntilDelimiterOrEof(&buf, '\n') catch break).?;
const time_line = (reader.readUntilDelimiterOrEof(&buf, '\n') catch break).?;
const task = Task{
.id = try std.fmt.parseInt(u32, id_line.?, 10),
.description = try self.allocator.dupe(u8, desc_line),
.priority = @enumFromInt(try std.fmt.parseInt(u2, priority_line, 10)),
.done = std.mem.eql(u8, done_line, "true"),
.created_at = try std.fmt.parseInt(i64, time_line, 10),
};
try self.tasks.append(task);
if (task.id >= self.next_id) {
self.next_id = task.id + 1;
}
}
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.){};
defer {
const status = gpa.deinit();
if (status == .leak) @panic("Memory leak detected!");
}
const allocator = gpa.allocator();
var store = TaskStore.init(allocator);
defer store.deinit();
try store.load();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
const stdout = std.io.getStdOut().writer();
if (args.len < 2) {
try printHelp(stdout);
return;
}
const cmd = args[1];
if (std.mem.eql(u8, cmd, "add")) {
try cmdAdd(allocator, &store, args[2..]);
} else if (std.mem.eql(u8, cmd, "list")) {
try cmdList(stdout, &store);
} else if (std.mem.eql(u8, cmd, "done")) {
try cmdDone(stdout, &store, args[2..]);
} else if (std.mem.eql(u8, cmd, "rm")) {
try cmdRemove(stdout, &store, args[2..]);
} else if (std.mem.eql(u8, cmd, "clear")) {
try cmdClear(stdout, &store);
} else if (std.mem.eql(u8, cmd, "help") or std.mem.eql(u8, cmd, "--help") or std.mem.eql(u8, cmd, "-h")) {
try printHelp(stdout);
} else {
try stdout.print("{s}Erro:{s} Comando desconhecido '{s}'\n\n", .{ ANSI.red, ANSI.reset, cmd });
try printHelp(stdout);
std.process.exit(1);
}
try store.save();
}
fn cmdAdd(allocator: std.mem.Allocator, store: *TaskStore, args: []const []const u8) !void {
if (args.len == 0) {
std.debug.print("{s}Erro:{s} Descrição necessária\n", .{ ANSI.red, ANSI.reset });
std.debug.print("Uso: taskman add <descrição> [-p high]\n", .{});
std.process.exit(1);
}
// Parse flags
var priority = Task.Priority.medium;
var description_parts = std.ArrayList([]const u8).init(allocator);
defer description_parts.deinit();
var i: usize = 0;
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "-p") or std.mem.eql(u8, args[i], "--priority")) {
if (i + 1 < args.len) {
priority = Task.Priority.fromString(args[i + 1]);
i += 1;
}
} else {
try description_parts.append(args[i]);
}
}
// Juntar descrição
const description = try std.mem.join(allocator, " ", description_parts.items);
defer allocator.free(description);
try store.add(description, priority);
std.debug.print("{s}✓{s} Tarefa adicionada (ID: {d}, Prioridade: {s}{s}{s})\n", .{
ANSI.green, ANSI.reset,
store.next_id - 1,
priority.color(), priority.label(), ANSI.reset,
});
}
fn cmdList(stdout: anytype, store: *const TaskStore) !void {
if (store.tasks.items.len == 0) {
try stdout.writeAll(ANSI.yellow ++ "Nenhuma tarefa encontrada." ++ ANSI.reset ++ "\n");
return;
}
try stdout.print("\n{s}╔════════════════════════════════════════════════╗{s}\n", .{ ANSI.bold, ANSI.reset });
try stdout.print("{s}║ GERENCIADOR DE TAREFAS ║{s}\n", .{ ANSI.bold, ANSI.reset });
try stdout.print("{s}╚════════════════════════════════════════════════╝{s}\n\n", .{ ANSI.bold, ANSI.reset });
var pending: usize = 0;
var completed: usize = 0;
for (store.tasks.items) |task| {
if (task.done) {
completed += 1;
} else {
pending += 1;
}
const status_icon = if (task.done) ANSI.green ++ "✓" ++ ANSI.reset else ANSI.yellow ++ "○" ++ ANSI.reset;
const desc_style = if (task.done) "\x1b[9m" ++ ANSI.cyan else ANSI.cyan;
try stdout.print("{s} [{d}] {s}{s}{s} {s}({s}){s}\n", .{
status_icon,
task.id,
desc_style,
task.description,
ANSI.reset,
task.priority.color(),
task.priority.label(),
ANSI.reset,
});
}
try stdout.print("\n{s}Resumo:{s} {s}{d} pendentes{s} | {s}{d} concluídas{s} | {s}{d} total{s}\n", .{
ANSI.bold, ANSI.reset,
ANSI.yellow, pending, ANSI.reset,
ANSI.green, completed, ANSI.reset,
ANSI.cyan, store.tasks.items.len, ANSI.reset,
});
}
fn cmdDone(stdout: anytype, store: *TaskStore, args: []const []const u8) !void {
if (args.len == 0) {
try stdout.print("{s}Erro:{s} ID da tarefa necessário\n", .{ ANSI.red, ANSI.reset });
return;
}
const id = try std.fmt.parseInt(u32, args[0], 10);
if (store.done(id)) {
try stdout.print("{s}✓{s} Tarefa {d} marcada como concluída\n", .{ ANSI.green, ANSI.reset, id });
} else {
try stdout.print("{s}✗{s} Tarefa {d} não encontrada\n", .{ ANSI.red, ANSI.reset, id });
}
}
fn cmdRemove(stdout: anytype, store: *TaskStore, args: []const []const u8) !void {
if (args.len == 0) {
try stdout.print("{s}Erro:{s} ID da tarefa necessário\n", .{ ANSI.red, ANSI.reset });
return;
}
const id = try std.fmt.parseInt(u32, args[0], 10);
if (store.remove(id)) {
try stdout.print("{s}✓{s} Tarefa {d} removida\n", .{ ANSI.green, ANSI.reset, id });
} else {
try stdout.print("{s}✗{s} Tarefa {d} não encontrada\n", .{ ANSI.red, ANSI.reset, id });
}
}
fn cmdClear(stdout: anytype, store: *TaskStore) !void {
for (store.tasks.items) |task| {
store.allocator.free(task.description);
}
store.tasks.clearRetainingCapacity();
store.next_id = 1;
try stdout.print("{s}✓{s} Todas as tarefas foram removidas\n", .{ ANSI.green, ANSI.reset });
}
fn printHelp(stdout: anytype) !void {
try stdout.print(
\\
{s}Taskman{s} - {s}Gerenciador de Tarefas para Terminal{s}
\\
\\Uso:{s} taskman <comando> [args] [flags]
\\
\\Comandos:{s}
\\ {s}add{s} <desc> [-p high] Adicionar nova tarefa
\\ {s}list{s} Listar todas as tarefas
\\ {s}done{s} <id> Marcar tarefa como concluída
\\ {s}rm{s} <id> Remover tarefa
\\ {s}clear{s} Remover todas as tarefas
\\ {s}help{s} Mostrar esta ajuda
\\
\\Flags:{s}
\\ {s}-p, --priority{s} <low|medium|high> Definir prioridade
\\
\\Exemplos:{s}
\\ taskman add "Implementar feature X" -p high
\\ taskman add "Revisar código" --priority low
\\ taskman list
\\ taskman done 1
\\ taskman rm 2
\\
, .{
ANSI.bold, ANSI.reset, ANSI.cyan, ANSI.reset,
ANSI.bold, ANSI.reset,
ANSI.bold, ANSI.reset,
ANSI.green, ANSI.reset,
ANSI.blue, ANSI.reset,
ANSI.yellow, ANSI.reset,
ANSI.red, ANSI.reset,
ANSI.magenta, ANSI.reset,
ANSI.cyan, ANSI.reset,
ANSI.bold, ANSI.reset,
ANSI.yellow, ANSI.reset,
ANSI.bold, ANSI.reset,
});
}
Exercícios Práticos
Exercício 1: Adicionar Categoria
Adicione suporte a categorias nas tarefas (trabalho, pessoal, estudos):
// Dica: Adicione um campo 'category' na struct Task
// e um enum Category similar a Priority
Ver solução
const Category = enum {
work,
personal,
study,
pub fn label(self: Category) []const u8 {
return switch (self) {
.work => "trabalho",
.personal => "pessoal",
.study => "estudos",
};
}
};
// Adicione na struct Task:
// category: Category,
// Parse com flag -c ou --category
Exercício 2: Filtro por Status
Adicione opção para listar apenas tarefas pendentes ou concluídas:
taskman list --pending
taskman list --done
Exercício 3: Editar Tarefa
Adicione um comando edit para modificar a descrição de uma tarefa existente.
Conclusão
Neste tutorial, você aprendeu a:
✅ Ler argumentos da linha de comando
✅ Parsear flags e argumentos posicionais
✅ Criar subcomandos
✅ Adicionar cores ao terminal
✅ Persistir dados em arquivo
✅ Criar uma CLI completa e profissional
Próximos Passos
- 📦 Como Instalar o Zig — revise os conceitos básicos
- 🔧 Zig Build System — domine o
build.zigpara configurar builds, dependências e distribuição da sua CLI - ⚡ Comptime em Zig — aprenda metaprogramação para CLIs avançadas
- 🔄 Tratamento de Erros em Zig — melhore o error handling da sua CLI
Criou uma CLI interessante com Zig? Compartilhe com a comunidade!
Testando Aplicações CLI em Zig
Testar aplicações de linha de comando pode parecer desafiador, mas Zig oferece ferramentas elegantes para isso. Vamos ver como testar cada aspecto da nossa CLI taskman.
Estratégia de Testes para CLI
Existem três níveis de testes para CLIs:
- Testes Unitários — Testam funções individuais (parsing, validação)
- Testes de Integração — Testam comandos completos com saída capturada
- Testes End-to-End — Executam o binário real em subprocesso
Testes Unitários
Vamos testar as funções puras da nossa CLI:
// src/main.zig (adicionar no final)
// ============ TESTES UNITÁRIOS ============
const testing = std.testing;
test "Priority.fromString" {
try testing.expectEqual(Task.Priority.low, Task.Priority.fromString("low"));
try testing.expectEqual(Task.Priority.low, Task.Priority.fromString("baixa"));
try testing.expectEqual(Task.Priority.high, Task.Priority.fromString("high"));
try testing.expectEqual(Task.Priority.high, Task.Priority.fromString("alta"));
try testing.expectEqual(Task.Priority.medium, Task.Priority.fromString("medium"));
try testing.expectEqual(Task.Priority.medium, Task.Priority.fromString("qualquer_coisa"));
}
test "TaskStore.add e persistência" {
const allocator = testing.allocator;
var store = TaskStore.init(allocator);
defer store.deinit();
// Adiciona tarefa
try store.add("Testar CLI", Task.Priority.high);
try testing.expectEqual(@as(u32, 1), store.next_id);
try testing.expectEqual(@as(usize, 1), store.tasks.items.len);
// Verifica tarefa adicionada
const task = store.tasks.items[0];
try testing.expectEqual(@as(u32, 1), task.id);
try testing.expectEqualStrings("Testar CLI", task.description);
try testing.expectEqual(Task.Priority.high, task.priority);
try testing.expect(!task.done);
// Testa persistência
try store.save();
// Cria novo store e carrega
var store2 = TaskStore.init(allocator);
defer store2.deinit();
try store2.load();
try testing.expectEqual(@as(usize, 1), store2.tasks.items.len);
try testing.expectEqualStrings("Testar CLI", store2.tasks.items[0].description);
}
test "TaskStore.done marca tarefa como concluída" {
const allocator = testing.allocator;
var store = TaskStore.init(allocator);
defer store.deinit();
try store.add("Tarefa teste", Task.Priority.medium);
try testing.expect(!store.tasks.items[0].done);
const result = store.done(1);
try testing.expect(result);
try testing.expect(store.tasks.items[0].done);
// Tarefa inexistente
const result2 = store.done(999);
try testing.expect(!result2);
}
test "TaskStore.remove deleta tarefa" {
const allocator = testing.allocator;
var store = TaskStore.init(allocator);
defer store.deinit();
try store.add("Tarefa 1", Task.Priority.low);
try store.add("Tarefa 2", Task.Priority.low);
try testing.expectEqual(@as(usize, 2), store.tasks.items.len);
const result = store.remove(1);
try testing.expect(result);
try testing.expectEqual(@as(usize, 1), store.tasks.items.len);
try testing.expectEqual(@as(u32, 2), store.tasks.items[0].id);
}
Execute os testes unitários:
zig test src/main.zig
Testes de Integração com stdout Capturado
Para testar a saída da CLI, podemos usar um ArrayList como writer:
// tests/integration_tests.zig
const std = @import("std");
const testing = std.testing;
// Simula a execução de um comando e captura a saída
fn runCommand(
allocator: std.mem.Allocator,
stdout: anytype,
args: []const []const u8,
) !void {
// Simula o comportamento do main()
var store = @import("../src/main.zig").TaskStore.init(allocator);
defer store.deinit();
try store.load();
if (args.len == 0) {
try @import("../src/main.zig").printHelp(stdout);
return;
}
const cmd = args[0];
if (std.mem.eql(u8, cmd, "list")) {
try @import("../src/main.zig").cmdList(stdout, &store);
} else if (std.mem.eql(u8, cmd, "help")) {
try @import("../src/main.zig").printHelp(stdout);
}
// ... outros comandos
}
test "list mostra mensagem quando não há tarefas" {
const allocator = testing.allocator;
var output = std.ArrayList(u8).init(allocator);
defer output.deinit();
const writer = output.writer();
try runCommand(allocator, writer, &.{"list"});
const output_str = output.items;
try testing.expect(std.mem.indexOf(u8, output_str, "Nenhuma tarefa") != null);
}
test "help mostra todas as opções" {
const allocator = testing.allocator;
var output = std.ArrayList(u8).init(allocator);
defer output.deinit();
const writer = output.writer();
try runCommand(allocator, writer, &.{"help"});
const output_str = output.items;
try testing.expect(std.mem.indexOf(u8, output_str, "add") != null);
try testing.expect(std.mem.indexOf(u8, output_str, "list") != null);
try testing.expect(std.mem.indexOf(u8, output_str, "done") != null);
try testing.expect(std.mem.indexOf(u8, output_str, "rm") != null);
}
Testes End-to-End com Subprocesso
Para testes que executam o binário real:
// tests/e2e_tests.zig
const std = @import("std");
const testing = std.testing;
fn runBinary(allocator: std.mem.Allocator, args: []const []const u8) !struct {
stdout: []u8,
stderr: []u8,
term: std.process.Child.Term,
} {
const binary_path = "../zig-out/bin/taskman";
var child = std.process.Child.init(
&([_][]const u8{binary_path} ++ args),
allocator,
);
child.stdout_behavior = .Pipe;
child.stderr_behavior = .Pipe;
try child.spawn();
const stdout = try child.stdout.?.reader().readAllAlloc(allocator, 1024 * 1024);
errdefer allocator.free(stdout);
const stderr = try child.stderr.?.reader().readAllAlloc(allocator, 1024 * 1024);
errdefer allocator.free(stderr);
const term = try child.wait();
return .{
.stdout = stdout,
.stderr = stderr,
.term = term,
};
}
test "binário retorna erro quando comando é desconhecido" {
const allocator = testing.allocator;
const result = try runBinary(allocator, &.{"comando_invalido"});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
try testing.expectEqual(@as(u8, 1), result.term.Exited);
try testing.expect(std.mem.indexOf(u8, result.stdout, "Erro") != null);
}
test "binário mostra help sem argumentos" {
const allocator = testing.allocator;
const result = try runBinary(allocator, &.{});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
try testing.expectEqual(@as(u8, 0), result.term.Exited);
try testing.expect(std.mem.indexOf(u8, result.stdout, "Taskman") != null);
}
Testando Parsing de Argumentos
// src/args.zig - Módulo separado para parsing de argumentos
const std = @import("std");
pub const Args = struct {
command: []const u8,
positional: []const []const u8,
flags: std.StringHashMap([]const u8),
pub fn parse(allocator: std.mem.Allocator, args: []const []const u8) !Args {
if (args.len < 2) return error.MissingCommand;
var flags = std.StringHashMap([]const u8).init(allocator);
errdefer flags.deinit();
var positional = std.ArrayList([]const u8).init(allocator);
errdefer positional.deinit();
var i: usize = 2;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.startsWith(u8, arg, "--")) {
const name = arg[2..];
if (i + 1 < args.len) {
try flags.put(name, args[i + 1]);
i += 1;
}
} else if (std.mem.startsWith(u8, arg, "-") and arg.len > 1) {
const name = arg[1..];
if (i + 1 < args.len) {
try flags.put(name, args[i + 1]);
i += 1;
}
} else {
try positional.append(arg);
}
}
return Args{
.command = args[1],
.positional = try positional.toOwnedSlice(),
.flags = flags,
};
}
pub fn deinit(self: *Args) void {
self.flags.deinit();
}
};
// ============ TESTES ============
const testing = std.testing;
test "Args.parse extrai comando corretamente" {
const allocator = testing.allocator;
var args = try Args.parse(allocator, &.{"taskman", "add", "descricao"});
defer args.deinit();
try testing.expectEqualStrings("add", args.command);
try testing.expectEqual(@as(usize, 1), args.positional.len);
try testing.expectEqualStrings("descricao", args.positional[0]);
}
test "Args.parse extrai flags longas" {
const allocator = testing.allocator;
var args = try Args.parse(allocator, &.{
"taskman", "add", "desc",
"--priority", "high",
"--category", "work"
});
defer args.deinit();
try testing.expectEqualStrings("high", args.flags.get("priority").?);
try testing.expectEqualStrings("work", args.flags.get("category").?);
}
test "Args.parse extrai flags curtas" {
const allocator = testing.allocator;
var args = try Args.parse(allocator, &.{
"taskman", "add", "desc",
"-p", "high",
"-c", "work"
});
defer args.deinit();
try testing.expectEqualStrings("high", args.flags.get("p").?);
try testing.expectEqualStrings("work", args.flags.get("c").?);
}
test "Args.parse retorna erro quando falta comando" {
const allocator = testing.allocator;
const result = Args.parse(allocator, &.{"taskman"});
try testing.expectError(error.MissingCommand, result);
}
Configurando o build.zig para Testes
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Executável principal
const exe = b.addExecutable(.{
.name = "taskman",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
// Testes unitários (no src/main.zig)
const unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
// Testes de integração
const integration_tests = b.addTest(.{
.root_source_file = b.path("tests/integration_tests.zig"),
.target = target,
.optimize = optimize,
});
const run_integration_tests = b.addRunArtifact(integration_tests);
// Step para rodar todos os testes
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_unit_tests.step);
test_step.dependOn(&run_integration_tests.step);
// Step para rodar só unitários
const unit_test_step = b.step("test-unit", "Run unit tests only");
unit_test_step.dependOn(&run_unit_tests.step);
// Step para rodar só integração
const integration_test_step = b.step("test-integration", "Run integration tests only");
integration_test_step.dependOn(&run_integration_tests.step);
}
Executando os testes:
# Todos os testes
zig build test
# Só unitários
zig build test-unit
# Só integração
zig build test-integration
# Com saída detalhada
zig test src/main.zig --test-cmd echo --test-cmd-bin
Dicas para Testes de CLI
- Isole funções puras — Funções que não fazem I/O são mais fáceis de testar
- Use dependency injection — Passe writers/readers como parâmetros
- Mock o sistema de arquivos — Use diretórios temporários nos testes
- Teste edge cases — Argumentos vazios, caracteres especiais, muitos argumentos
- Verifique códigos de saída — Sucesso (0) vs erro (1+)
Exemplo Completo: CLI Testável
// Padrão para CLI testável
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
return runCli(allocator, stdout, stderr, args);
}
// Função testável — recebe todas as dependências
pub fn runCli(
allocator: std.mem.Allocator,
stdout: anytype,
stderr: anytype,
args: []const []const u8,
) !void {
if (args.len < 2) {
try printHelp(stdout);
return;
}
// ... resto da lógica
}
// Nos testes:
test "runCli com argumentos vazios mostra help" {
var output = std.ArrayList(u8).init(testing.allocator);
defer output.deinit();
var errors = std.ArrayList(u8).init(testing.allocator);
defer errors.deinit();
try runCli(testing.allocator, output.writer(), errors.writer(), &.{});
try testing.expect(std.mem.indexOf(u8, output.items, "Uso:") != null);
}