← Volver al listado de tecnologías
Proyecto Final: CLI en Zig
Proyecto Final: CLI de Gestión de Tareas
Descripción
Construiremos ztask, una herramienta CLI para gestionar tareas que demuestra:
- Parsing de argumentos
- Manejo de archivos
- Serialización JSON
- Manejo de errores
- Testing completo
- Build system
Estructura del Proyecto
ztask/
├── build.zig
├── src/
│ ├── main.zig
│ ├── task.zig
│ ├── storage.zig
│ ├── cli.zig
│ └── lib.zig
└── tests/
└── test_task.zig
Modelo de Datos (task.zig)
const std = @import("std");
pub const Priority = enum {
low,
medium,
high,
pub fn toString(self: Priority) []const u8 {
return switch (self) {
.low => "baja",
.medium => "media",
.high => "alta",
};
}
pub fn fromString(s: []const u8) ?Priority {
if (std.mem.eql(u8, s, "low") or std.mem.eql(u8, s, "baja")) return .low;
if (std.mem.eql(u8, s, "medium") or std.mem.eql(u8, s, "media")) return .medium;
if (std.mem.eql(u8, s, "high") or std.mem.eql(u8, s, "alta")) return .high;
return null;
}
};
pub const Task = struct {
id: u32,
titulo: []const u8,
descripcion: ?[]const u8,
prioridad: Priority,
completada: bool,
creada: i64,
pub fn crear(allocator: std.mem.Allocator, id: u32, titulo: []const u8) !Task {
const titulo_copia = try allocator.dupe(u8, titulo);
return Task{
.id = id,
.titulo = titulo_copia,
.descripcion = null,
.prioridad = .medium,
.completada = false,
.creada = std.time.timestamp(),
};
}
pub fn deinit(self: *Task, allocator: std.mem.Allocator) void {
allocator.free(self.titulo);
if (self.descripcion) |desc| {
allocator.free(desc);
}
}
pub fn completar(self: *Task) void {
self.completada = true;
}
pub fn setPrioridad(self: *Task, prioridad: Priority) void {
self.prioridad = prioridad;
}
};
pub const TaskList = struct {
tasks: std.ArrayList(Task),
next_id: u32,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) TaskList {
return .{
.tasks = std.ArrayList(Task).init(allocator),
.next_id = 1,
.allocator = allocator,
};
}
pub fn deinit(self: *TaskList) void {
for (self.tasks.items) |*task| {
task.deinit(self.allocator);
}
self.tasks.deinit();
}
pub fn agregar(self: *TaskList, titulo: []const u8) !*Task {
const task = try Task.crear(self.allocator, self.next_id, titulo);
try self.tasks.append(task);
self.next_id += 1;
return &self.tasks.items[self.tasks.items.len - 1];
}
pub fn buscar(self: *TaskList, id: u32) ?*Task {
for (self.tasks.items) |*task| {
if (task.id == id) return task;
}
return null;
}
pub fn eliminar(self: *TaskList, id: u32) bool {
for (self.tasks.items, 0..) |*task, i| {
if (task.id == id) {
task.deinit(self.allocator);
_ = self.tasks.orderedRemove(i);
return true;
}
}
return false;
}
pub fn listar(self: *TaskList, solo_pendientes: bool) []Task {
if (!solo_pendientes) return self.tasks.items;
// Filtrar pendientes requeriría allocator, simplificamos
return self.tasks.items;
}
};
Almacenamiento (storage.zig)
const std = @import("std");
const Task = @import("task.zig").Task;
const TaskList = @import("task.zig").TaskList;
const Priority = @import("task.zig").Priority;
pub const StorageError = error{
FileNotFound,
ParseError,
WriteError,
};
pub fn guardar(list: *TaskList, path: []const u8) !void {
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
var writer = file.writer();
try writer.writeAll("[\n");
for (list.tasks.items, 0..) |task, i| {
try writer.print(
\\ {{
\\ "id": {d},
\\ "titulo": "{s}",
\\ "prioridad": "{s}",
\\ "completada": {s}
\\ }}
, .{
task.id,
task.titulo,
task.prioridad.toString(),
if (task.completada) "true" else "false",
});
if (i < list.tasks.items.len - 1) {
try writer.writeAll(",\n");
} else {
try writer.writeAll("\n");
}
}
try writer.writeAll("]\n");
}
pub fn cargar(allocator: std.mem.Allocator, path: []const u8) !TaskList {
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
if (err == error.FileNotFound) {
return TaskList.init(allocator);
}
return err;
};
defer file.close();
var list = TaskList.init(allocator);
// Parser JSON simplificado
const contenido = try file.readToEndAlloc(allocator, 1024 * 1024);
defer allocator.free(contenido);
// Implementación simplificada - en producción usar std.json
_ = contenido;
return list;
}
pub fn obtenerRutaDefault(allocator: std.mem.Allocator) ![]const u8 {
if (std.posix.getenv("HOME")) |home| {
return try std.fmt.allocPrint(allocator, "{s}/.ztasks.json", .{home});
}
return try allocator.dupe(u8, ".ztasks.json");
}
CLI Parser (cli.zig)
const std = @import("std");
pub const Comando = union(enum) {
agregar: []const u8,
listar: struct { pendientes: bool },
completar: u32,
eliminar: u32,
ayuda,
version,
};
pub const ParseError = error{
ComandoDesconocido,
ArgumentoFaltante,
IdInvalido,
};
pub fn parsear(args: []const []const u8) ParseError!Comando {
if (args.len < 2) return .ayuda;
const comando = args[1];
if (std.mem.eql(u8, comando, "add") or std.mem.eql(u8, comando, "agregar")) {
if (args.len < 3) return ParseError.ArgumentoFaltante;
return .{ .agregar = args[2] };
}
if (std.mem.eql(u8, comando, "list") or std.mem.eql(u8, comando, "listar")) {
const pendientes = args.len > 2 and
(std.mem.eql(u8, args[2], "--pending") or
std.mem.eql(u8, args[2], "-p"));
return .{ .listar = .{ .pendientes = pendientes } };
}
if (std.mem.eql(u8, comando, "done") or std.mem.eql(u8, comando, "completar")) {
if (args.len < 3) return ParseError.ArgumentoFaltante;
const id = std.fmt.parseInt(u32, args[2], 10) catch {
return ParseError.IdInvalido;
};
return .{ .completar = id };
}
if (std.mem.eql(u8, comando, "rm") or std.mem.eql(u8, comando, "eliminar")) {
if (args.len < 3) return ParseError.ArgumentoFaltante;
const id = std.fmt.parseInt(u32, args[2], 10) catch {
return ParseError.IdInvalido;
};
return .{ .eliminar = id };
}
if (std.mem.eql(u8, comando, "help") or std.mem.eql(u8, comando, "--help")) {
return .ayuda;
}
if (std.mem.eql(u8, comando, "version") or std.mem.eql(u8, comando, "--version")) {
return .version;
}
return ParseError.ComandoDesconocido;
}
pub fn mostrarAyuda() void {
const ayuda =
\\ztask - Gestor de tareas en Zig
\\
\\USO:
\\ ztask <comando> [opciones]
\\
\\COMANDOS:
\\ add <titulo> Agregar nueva tarea
\\ list [-p] Listar tareas (-p: solo pendientes)
\\ done <id> Marcar tarea como completada
\\ rm <id> Eliminar tarea
\\ help Mostrar esta ayuda
\\ version Mostrar versión
\\
\\EJEMPLOS:
\\ ztask add "Comprar leche"
\\ ztask list
\\ ztask done 1
\\
;
std.debug.print("{s}", .{ayuda});
}
Main (main.zig)
const std = @import("std");
const Task = @import("task.zig").Task;
const TaskList = @import("task.zig").TaskList;
const cli = @import("cli.zig");
const storage = @import("storage.zig");
const VERSION = "1.0.0";
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 comando = cli.parsear(args) catch |err| {
switch (err) {
cli.ParseError.ComandoDesconocido => {
std.debug.print("Error: Comando desconocido\n", .{});
},
cli.ParseError.ArgumentoFaltante => {
std.debug.print("Error: Argumento faltante\n", .{});
},
cli.ParseError.IdInvalido => {
std.debug.print("Error: ID inválido\n", .{});
},
}
cli.mostrarAyuda();
return;
};
const ruta = try storage.obtenerRutaDefault(allocator);
defer allocator.free(ruta);
var lista = try storage.cargar(allocator, ruta);
defer lista.deinit();
switch (comando) {
.agregar => |titulo| {
_ = try lista.agregar(titulo);
try storage.guardar(&lista, ruta);
std.debug.print("Tarea agregada\n", .{});
},
.listar => |opts| {
const tareas = lista.listar(opts.pendientes);
if (tareas.len == 0) {
std.debug.print("No hay tareas\n", .{});
return;
}
std.debug.print("\n{s:<4} {s:<30} {s:<10} {s}\n", .{
"ID", "TITULO", "PRIORIDAD", "ESTADO",
});
std.debug.print("{s}\n", .{"-" ** 60});
for (tareas) |task| {
const estado = if (task.completada) "[x]" else "[ ]";
std.debug.print("{d:<4} {s:<30} {s:<10} {s}\n", .{
task.id,
task.titulo,
task.prioridad.toString(),
estado,
});
}
},
.completar => |id| {
if (lista.buscar(id)) |task| {
task.completar();
try storage.guardar(&lista, ruta);
std.debug.print("Tarea {d} completada\n", .{id});
} else {
std.debug.print("Tarea {d} no encontrada\n", .{id});
}
},
.eliminar => |id| {
if (lista.eliminar(id)) {
try storage.guardar(&lista, ruta);
std.debug.print("Tarea {d} eliminada\n", .{id});
} else {
std.debug.print("Tarea {d} no encontrada\n", .{id});
}
},
.ayuda => cli.mostrarAyuda(),
.version => std.debug.print("ztask v{s}\n", .{VERSION}),
}
}
Tests (tests/test_task.zig)
const std = @import("std");
const testing = std.testing;
const Task = @import("../src/task.zig").Task;
const TaskList = @import("../src/task.zig").TaskList;
const Priority = @import("../src/task.zig").Priority;
const cli = @import("../src/cli.zig");
test "crear tarea" {
var task = try Task.crear(testing.allocator, 1, "Test task");
defer task.deinit(testing.allocator);
try testing.expectEqual(@as(u32, 1), task.id);
try testing.expectEqualStrings("Test task", task.titulo);
try testing.expect(!task.completada);
}
test "completar tarea" {
var task = try Task.crear(testing.allocator, 1, "Test");
defer task.deinit(testing.allocator);
try testing.expect(!task.completada);
task.completar();
try testing.expect(task.completada);
}
test "TaskList agregar y buscar" {
var lista = TaskList.init(testing.allocator);
defer lista.deinit();
_ = try lista.agregar("Tarea 1");
_ = try lista.agregar("Tarea 2");
try testing.expectEqual(@as(usize, 2), lista.tasks.items.len);
const encontrada = lista.buscar(1);
try testing.expect(encontrada != null);
try testing.expectEqualStrings("Tarea 1", encontrada.?.titulo);
}
test "TaskList eliminar" {
var lista = TaskList.init(testing.allocator);
defer lista.deinit();
_ = try lista.agregar("Tarea 1");
_ = try lista.agregar("Tarea 2");
try testing.expect(lista.eliminar(1));
try testing.expectEqual(@as(usize, 1), lista.tasks.items.len);
try testing.expect(lista.buscar(1) == null);
}
test "Priority fromString" {
try testing.expectEqual(Priority.low, Priority.fromString("low").?);
try testing.expectEqual(Priority.high, Priority.fromString("alta").?);
try testing.expect(Priority.fromString("invalid") == null);
}
test "CLI parsear add" {
const args = &[_][]const u8{ "ztask", "add", "Nueva tarea" };
const cmd = try cli.parsear(args);
switch (cmd) {
.agregar => |titulo| {
try testing.expectEqualStrings("Nueva tarea", titulo);
},
else => try testing.expect(false),
}
}
test "CLI parsear list" {
const args = &[_][]const u8{ "ztask", "list" };
const cmd = try cli.parsear(args);
switch (cmd) {
.listar => |opts| {
try testing.expect(!opts.pendientes);
},
else => try testing.expect(false),
}
}
test "CLI parsear done" {
const args = &[_][]const u8{ "ztask", "done", "5" };
const cmd = try cli.parsear(args);
switch (cmd) {
.completar => |id| {
try testing.expectEqual(@as(u32, 5), id);
},
else => try testing.expect(false),
}
}
test "CLI comando desconocido" {
const args = &[_][]const u8{ "ztask", "invalid" };
const result = cli.parsear(args);
try testing.expectError(cli.ParseError.ComandoDesconocido, result);
}
Build (build.zig)
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "ztask",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
const run_step = b.step("run", "Ejecutar ztask");
run_step.dependOn(&run_cmd.step);
// Tests de módulos
const task_tests = b.addTest(.{
.root_source_file = b.path("tests/test_task.zig"),
.target = target,
.optimize = optimize,
});
const test_step = b.step("test", "Ejecutar tests");
test_step.dependOn(&b.addRunArtifact(task_tests).step);
}
Uso
# Compilar
zig build
# Ejecutar
./zig-out/bin/ztask add "Aprender Zig"
./zig-out/bin/ztask add "Completar tutorial"
./zig-out/bin/ztask list
./zig-out/bin/ztask done 1
./zig-out/bin/ztask rm 2
# Tests
zig build test
Extensiones Sugeridas
- Persistencia real: Implementar parser JSON completo
- Fechas de vencimiento: Agregar campo de fecha límite
- Categorías: Organizar tareas por proyecto
- Colores: Colorear salida según prioridad
- Exportar: Generar reportes en Markdown
Resumen del Tutorial
Has aprendido:
- Sintaxis básica y tipos de Zig
- Control de flujo y funciones
- Arrays, slices y strings
- Structs, enums y unions
- Gestión de memoria con allocators
- Manejo de errores robusto
- Metaprogramación con comptime
- Interoperabilidad con C
- Concurrencia y threads
- Build system completo
- Testing en cada paso
¡Felicitaciones por completar el tutorial de Zig!