¡Hola a todos! 👋 Hoy vamos a hablar de un tema muy interesante: cómo analizar los argumentos de la línea de comandos. ¡Prepárate para una profunda inmersión! 🌊🏊♂️
Introducción a los Argumentos de la Línea de Comandos 📝
Los argumentos de la línea de comandos son una forma común de especificar opciones para programas ejecutables. Son especialmente útiles para interactuar con tu programa de forma flexible y potente. En esencia, los argumentos de la línea de comandos son una lista de palabras que se pasan a un programa cuando se invoca. ¿Pero cómo se manejan en Zig? ¡Vamos a descubrirlo! 🕵️♀️
Argumentos de la Línea de Comandos en Zig 🧮
En Zig, la sintaxis para los argumentos de la línea de comandos es bastante sencilla. Cuando se inicia un programa, los argumentos de la línea de comandos se pasan como una matriz de cadenas. Puedes recuperar argumentos de la línea de comandos usando la estructura std.process.ArgIterator
e inicializándola con initWithAllocator(allocator: Allocator)
si queremos abstraernos de la plataforma. Con este iterador podemos recorrer los argumentos, pero a menudo necesitamos un poco más de funcionalidad.
var it = try std.process.ArgIterator.initWithAllocator(allocator);
defer it.deinit();
while (it.next()) |arg| {
std.debug.print("{s}\n", .{arg});
}
También podemos usar std.process.ArgIterator.init()
sin necesidad de allocator pero en la práctica la opción anterior es la más recomendable, teniendo además en cuenta que el uso de un parsing más sofisticado de los argumentos casi seguro requerirá de un asignador de memoria.
Próximamente: Aprende sobre allocators en el artículo “Memoria en Zig”
Bibliotecas de análisis
Es cuando necesitamos más funcionalidad cuando entran las bibliotecas de análisis como ‘zig-clap’ o ‘zig-args’:
Próximamente: Aprende sobre el uso de librerías en Zig en el artículo “Las librerías de Zig”
- zig-clap: Es una biblioteca simple y fácil de usar para el análisis de argumentos de línea de comando en Zig. Ofrece características como argumentos cortos y largos, soporte para pasar valores usando espacios y ‘=’, opciones que pueden ser especificadas múltiples veces, impresión y análisis de mensajes de ayuda a partir de especificaciones de parámetros.
En esencia le pasamos una definición en forma de texto de cómo queremos parsear los argumentos y nos creará una estructura, en tiempo de compilación, para esa definición. Luego rellenaremos una instancia de la estructura con las opciones y argumentos pasados al programa.
Aprende todo sobre tiempo de compilación en “Zig comptime”
⚠️ ¡Atención, programador! Este post utiliza la versión 0.11.0-dev.3971 de Zig
const params = comptime clap.parseParamsComptime(
\\-h, --help Display this help and exit.
\\-n, --number <usize> An option parameter, which takes a value.
\\-s, --string <str>... An option parameter which can be specified multiple times.
\\<str>...
\\
);
var diag = clap.Diagnostic{};
var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{
.diagnostic = &diag,
}) catch |err| {
diag.report(std.io.getStdErr().writer(), err) catch {};
return err;
};
defer res.deinit();
if (res.args.help != 0)
return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{});
if (res.args.number) |n|
std.debug.print("--number = {}\n", .{n});
for (res.args.string) |s|
std.debug.print("--string = {s}\n", .{s});
std.debug.print("positional arguments:\n", .{});
for (res.positionals) |pos| {
std.debug.print("{s}\n", .{pos});
}
En ‘zig-clap’, puedes manejar casos más complejos, como subcomandos o argumentos opcionales y definir tu propio parsing de argumentos con clap.parsers.default
. 💡
Es esencial manejar errores al analizar argumentos de la línea de comandos. En ‘zig-clap’, puedes usar la estructura Diagnostics
para informar errores útiles. 🚑
- zig-args: Este es otro módulo de Zig para el análisis de argumentos de línea de comando. Proporciona funciones para analizar argumentos para una especificación dada y nuestro proceso actual, con opciones para manejar errores de análisis. También permite el uso de un método ‘Verb’, lo que significa que el primer argumento posicional se interpreta como un verbo que puede ser considerado un subcomando que proporciona más opciones específicas.
A diferencia de clap, definimos las opciones y argumentos en una estructura que se rellenará al analizar los argumentos.
const options = try argsParser.parseForCurrentProcess(struct {
// This declares long options for double hyphen
output: ?[]const u8 = null,
@"with-offset": bool = false,
@"with-hexdump": bool = false,
@"intermix-source": bool = false,
numberOfBytes: ?i32 = null,
signed_number: ?i64 = null,
unsigned_number: ?u64 = null,
mode: enum { default, special, slow, fast } = .default,
// This declares short-hand options for single hyphen
pub const shorthands = .{
.S = "intermix-source",
.b = "with-hexdump",
.O = "with-offset",
.o = "output",
};
}, allocator, .print);
defer options.deinit();
if (options.executable_name) |exe| {
std.debug.print("executable name: {s}\n", .{exe});
}
std.debug.print("parsed options:\n", .{});
inline for (std.meta.fields(@TypeOf(options.options))) |fld| {
std.debug.print("\t{s} = {any}\n", .{
fld.name,
@field(options.options, fld.name),
});
}
std.debug.print("parsed positionals:\n", .{});
for (options.positionals) |arg| {
std.debug.print("\t'{s}'\n", .{arg});
}
Nuestro propio analizador 🤓
Usar una biblioteca probada es lo que uno debe hacer normalmente y no inventar la rueda a la primera de cambio. Pero estamos aquí para aprender y divertirnos con Zig, así que no vamos a dejar pasar la oportunidad de implementar nuestro propio analizador de argumentos de línea de comando. ¡Sí, lo has oído bien! 🎉🎉.
Aprenderás una cantidad increíble en el proceso. Vas a entender mejor cómo funcionan las cosas bajo el capó, cómo los argumentos pasan de la línea de comandos a tu programa y cómo puedes manipularlos para hacer exactamente lo que quieres. ¡Es como tener superpoderes! 💪🚀 Así que, poneros cómodos, preparaos una taza de vuestro café o te favorito ☕ y ¡vamos a ello, creemos nuestra propia biblioteca de análisis de argumentos de línea de comando: zig-argueando! 🎯
Convenciones POSIX
Nuestro analizado implementará, en general, las convenciones recomendadas por POSIX para los argumentos de la línea de comandos:
- Los argumentos son opciones cortas si comienzan con un delimitador de guión (
-
) más un caracter alfanuméricos individuales u opviones largar si empiezan con doble guión (--
) con un nombre largo. - Varias opciones cortas pueden seguir a un delimitador de guión en un solo token si las opciones no toman argumentos. Por lo tanto,
-abc
es equivalente a-a -b -c
. La última opción si podrá tener argumento. - Algunas opciones requieren un argumento. Por ejemplo, la opción -o del comando
ld
requiere un argumento, un nombre de archivo de salida. - Las opciones normalmente preceden a otros argumentos que no son opciones y que se llaman posicionales.
- Los argumentos de las opciones pueden ir en el mismo token si la opción y su argumento está separado por un separador. Por ejemplo,
-a=foo
o--name=value
en lugar de-a foo
o--name value
- Un argumento que es exactamente doble guión (
--
) indica que a partir de ese argumentos todos los demás son posicionales. - Un argumento que es exactamente un guión (
-
) se tratará como posicional. Suele indicar entrada o salida estandar. - Las opciones se pueden suministrar en cualquier orden.
Pero nos saldremos un poco de las convenciones en estos aspectos, por otra parte discutibles:
- Una opción y su argumento pueden aparecer o no como tokens separados. En otras palabras, el espacio en blanco que los separa es opcional. Por lo tanto,
-o foo
y-ofoo
son equivalentes. Pero nuestro analizador no va a permitir esto ya que puede dar lugar a ambigüedades. - Los nombres de las opciones deben escribirse completamente y daremos la opción como desconocida si es parcial aunque no haya conflicto. Si la opción es
--host
no podrá escribirse como--hos
, ni--ho
, ni--h
. - Las opciones pueden aparecer varias veces pero sólo si son opciones que lo permiten explícitamente o tienen el mismo valor. Nada de la última opción gana, buff.
Necesitamos un plan
Estos son una serie de tareas iniciales para diseñar un parser de línea de comandos en Zig:
-
Diseñar la estructura de datos de los argumentos
Define una estructura de datos que represente un argumento de línea de comandos. Esta estructura debe contener información como el nombre corto de la opción (si existe), el nombre largo (si existe), si la opción necesita un valor, y cualquier valor predeterminado o proporcionado por el usuario.
-
Implementar la función de análisis de argumentos
Esta función debería tomar la lista de argumentos proporcionados por el usuario (generalmente a través de
std.process.args()
) y llenar la estructura de datos definida anteriormente. Debe ser capaz de manejar tanto argumentos cortos (-a
) como largos (--arg
), y debe manejar correctamente los argumentos que requieren valores (-o value
o-o value
o--option=value
). También debe ser capaz de manejar el caso especial de--
, que indica el final de los argumentos. -
Implementar el manejo de errores
Si el usuario proporciona un argumento que no se reconoce, o si se proporciona un valor para un argumento que no requiere uno, el parser debe ser capaz de manejar esto de manera elegante. Esto puede incluir la generación de mensajes de error útiles.
-
Implementar funciones de ayuda
Proporcionar funciones que generen automáticamente mensajes de ayuda basados en las estructuras de argumentos definidas. Esto puede incluir una descripción del comando y de cada argumento, si se requiere un valor, y cualquier valor predeterminado.
-
Pruebas
Probaremos completamente el parser de línea de comandos. Esto incluirá pruebas para argumentos cortos y largos, argumentos con y sin valores, el indicador
--
, y el manejo de errores.
Fin de la primera parte 🌟
No te pierdas las siguientes partes donde comenzaremos a implementar el analizador explicando paso a paso lo que hacemos. ¡Nos vemos! Un ejemplo de lo que vamos a programar:
const clp = comptime Argueando.CommandLineParser.init(.{
.header=
\\ \ |
\\ _ \ __| _` | | | _ \ _` | __ \ _` | _ \
\\ ___ \ | ( | | | __/ ( | | | ( | ( |
\\_/ _\ _| \__, | \__,_| \___| \__,_| _| _| \__,_| \___/
\\ |___/
,.params = &[_]Argueando.Param{
flagHelp(.{ .long = "help", .short = "h", .help = "Shows this help." }),
flag(.{ .long = "version", .help = "Output version information and exit." }),
flag(.{ .long = "verbose", .short = "v", .help = "Enable verbose output." }),
option(.{ .long = "port", .short = "p", .parser = "TCP_PORT", .default = "1234", .help = "Listening Port." }),
option(.{ .long = "host", .short = "H", .parser = "TCP_HOST", .default = "localhost", .help = "Host name" }),
singlePositional(.{ .parser = "DIR", .default = ".", .check = &Check.Dir(.{ .mode = .read_only }).f }),
}, //
.desc = "This command starts an HTTP Server and serves static content from directory DIR.", //
.footer = "More info: <https://d4c7.github.io/zig-zagueando/>.",
});
var s = clp.parseArgs(allocator);
defer s.deinit();
if (s.helpRequested()) {
try s.printHelp(std.io.getStdErr().writer());
return;
}
if (s.hasProblems()) {
try s.printProblems(std.io.getStdErr().writer(), .AllProblems);
return;
}
\ |
_ \ __| _` | | | _ \ _` | __ \ _` | _ \
___ \ | ( | | | __/ ( | | | ( | ( |
_/ _\ _| \__, | \__,_| \___| \__,_| _| _| \__,_| \___/
|___/
Usage: sample-argueando [(-h|--help)]
[--version] [(-v|--verbose)] [(-p|--port)=TCP_PORT] [(-H|--host)=TCP_HOST]
[DIR]
This command starts an HTTP Server and serves static content from directory DIR.
-h, --help Shows this help.
--version Output version information and exit.
-v, --verbose Enable verbose output.
-p, --port=TCP_PORT Listening Port.
Default value: 1234
-H, --host=TCP_HOST Host name
Default value: localhost
TCP_PORT TCP port value between 0 and 65535. Use port 0 to dynamically assign a port
Can use base prefix (0x,0o,0b).
TCP_HOST TCP host name or IP.
DIR Directory
More info: <https://d4c7.github.io/zig-zagueando/>.