From 3739f7e7b8dca3aa3b123059e4c0e894ca6b896c Mon Sep 17 00:00:00 2001 From: BitlyTwiser Date: Sun, 22 Sep 2024 11:00:17 -0700 Subject: [PATCH] Adjusted README to be more fine tuned and informational. Adjusting lib to mark selected API elemetns as pub. Adjusted main to showcase a quick example of parsing and API usage. Adjusted sneaky to perform most fundamental parsing operations --- README.md | 119 ++++++++++++++++++++++++----- src/lib.zig | 3 +- src/main.zig | 19 ++++- src/sneaky.zig | 203 +++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 300 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 0ceffa9..acab564 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,116 @@
-
-# snek -Snek - A simple CLI parser to build CLI applications in Zig +# ๐Ÿsnek๐Ÿ +A simple CLI parser building CLI applications in Zig ## Note: This is currently under construction and is not yet available for public consumption (hence no releases!) +# Contents +[Usage](#usage) +[Building the CLI](#build-your-cli) +[Examples](#examples) +[Optionals](#optionals) +[Default Values](#default-values) +[Help Menu](#help-menu) +[What is not supported](#what-is-not-supported) + + -### Usage: -Add snek to your zig project: +### Usage +Add snek to your Zig project with Zon: ``` zig fetch --save https://github.com/BitlyTwiser/snek/archive/refs/tags/0.1.0.tar.gz ``` -Add to build file: +Add the following to build.zig file: ``` const snek = b.dependency("snek", .{}); exe.root_module.addImport("snek", snek.module("snek")); ``` -### Build your CLI: -Snek builds dynamic (yet simple) CLI's using metadata programming to infer the struct fields, the expected types, then insert the incoming data from the stdin arguments and serialize that data into the given struct. +### Build your CLI +Snek builds dynamic (yet simple) CLI's using zigs meta programming to infer the struct fields, the expected types, then insert the incoming data from the stdin arguments and serialize that data into the given struct mapping the data values to the given fields and marshalling the data into the proper type. ``` const T = struct { bool_test: bool, word: []const u8, test_opt: ?u32, + test_default: []const u8 = "I am static if not set by user", }; var snek = try Snek(T).init(std.heap.page_allocator); - try snek.help(); + const parsed = try snek.parse(); + + // Do Stuff with the fields of the struct after parsing + std.debug.print("{any}", .{parsed}); ``` -#### Optionals: -Using zig optionals, you can set selected flags to be ignored if they are not present. +When the user goes to interact with the application, they can now utilize the flags you have established to run specific commands. -#### Default Values: -You can use struct defaut values to set a static value if one is not parsed. +#### Items to note: +1. If the user does not supply a value and the field is *not* otional, that is a failure case and a message is displayed to the user +2. If there is a default value on the field of the struct and a vale is not passed for that field, it is treated as an *optional* case and will use the static value (i.e. no error message and value is set) +3. Simple structs only for now, no recursive struct fields at the moment. (i.e. no embeded structs) +4. If the users passed the wrong *type* which differes from what is expeected (i.e. the type of the struct field), this is an error case and a message will be displayed to the user. +5. If you want to handle the errors yourself, the CliError struct is public, so you can catch errors on the `parse()` call +``` + const T = struct { + bool_test: bool, + word: []const u8, + test_opt: ?u32, + test_default: []const u8 = "I am static if not set by user", + }; + var snek = try Snek(T).init(std.heap.page_allocator); + // Adjust to actually use value of course + _ = snek.parse() catch |err| { + switch(e) { + ... do stuff with the Errors + } + } + +``` + + +#### Examples +Using the above struct as a reference, here are a few examples of calling the CLI: +##### Help +``` +./ -help + +# or + +./ -h +``` -### Help Menu: +Note: As you can see, the optionals are just that, *optional*. They are not required by your users and can be checked in the calling code in the standard ways that Zig handles optionals. +This is a design decisions allowing flexibility over the CLI to not lock users into using every flag etc.. +##### Optionals +```` +./ -bool_test=true -word="I am a word!" +```` + +##### Defaults: +``` +./ -bool_test=true -word="I am a word!" + +# or to override the default field + + +./ -bool_test=true -word="I am a word!" -test_defaults="I am a different word!" +``` + +#### Optionals +Using zig optionals, you can set selected flags to be ignored on the CLI, thus giving flexibilitiy on the behalf of the CLI creator to use or not use selected flags at their whimsy + +#### Default Values +You can use struct defaut values to set a static value if one is not parsed. This can be useful for certain flags for conditional logic branching later in program execution. + +### Help Menu Snek dynaically builds the help menu for your users. By calling the `help()` function, you can display how to use your CLI: ``` const T = struct { @@ -52,10 +119,8 @@ Snek dynaically builds the help menu for your users. By calling the `help()` fun test_opt: ?u32, }; var snek = try Snek(T).init(std.heap.page_allocator); - try snek.parse(); + const parsed = try snek.help(); - // Print the values from the serialized struct data - std.debug.print("{any}", .{T.bool_test}); ``` Output: ``` @@ -68,3 +133,23 @@ CLI Flags Help Menu ``` +Alternatively, if users call -help as the *first* arguments in the CLI, it will also display the help menu. +``` +./ -help + +# or + +./ -h +``` + +This will display the help menu and skip *all other parsing*. So its important to note that this is effectively an exit case for the parser and your program. +You should build your application to support this. + + +### What is *not* supported + +##### Recursive struct types for sub-command fields +At this time, no recursive flags are supported, i.e. you cannot use a slice of structs as a field in the primary CLI interface struct and have those fields parsed as sub-command fields. +Perhaps, if this is requested, we could work that into the application. It seemed slightly messy and unecessray for a simple CLI builder, but perhaps expansion will be necessary there if its requested :) + +[Top](#usage) \ No newline at end of file diff --git a/src/lib.zig b/src/lib.zig index 464378e..e50f10a 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -1,2 +1,3 @@ const snek = @import("sneaky.zig"); -const Snek = snek.Snek; +pub const Snek = snek.Snek; +pub const CliError = snek.CliError; // Exposes the Error strcut type to allow users different error handling if desired diff --git a/src/main.zig b/src/main.zig index 13ab026..2b36162 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,3 +1,20 @@ const std = @import("std"); +const snek = @import("lib.zig").Snek; -pub fn main() !void {} +// Binary is also compiled for showcasing how to use the API +const T = struct { + name: []const u8, + location: u32, + exists: bool, + necessary: ?bool, + filled_optional: ?[]const u8, + default_name: []const u8 = "test default name", +}; + +pub fn main() !void { + var cli = try snek(T).init(std.heap.page_allocator); + const parsed_cli = try cli.parse(); + + // Necessary is skipped here + std.debug.print("{s} {d} {any} {s} {s}", .{ parsed_cli.name, parsed_cli.location, parsed_cli.exists, parsed_cli.default_name, if (parsed_cli.filled_optional) |filled| filled orelse "badvalue" }); +} diff --git a/src/sneaky.zig b/src/sneaky.zig index 94c2770..57c0772 100644 --- a/src/sneaky.zig +++ b/src/sneaky.zig @@ -2,22 +2,22 @@ const std = @import("std"); const builtin = @import("builtin"); +const assert = std.debug.assert; -// cli tool that supports arguments like -cooamdn=asdasd -// Supports optional arguments (use optionals in struct to determine if they are required or not) -// Supports default arguments if there is an existing value in the struct. -// If the stdin args do not match or are missing (no optional) throw an error -// Actually the return data IS the input struct. Like yamlz. I.e. the struct will hold default fields if non are parsed (has to be optionals) -// in the end we just return the struct -// no sub-commands as of now i.e. thing -thing=asdasd etc.. just flat commands for now -// Dagger - A tool for building robust CLI tools in Zig +pub const CliError = error{ InvalidArg, InvalidNumberOfArgs, CliArgumentNotFound, InvalidCommand, HelpCommand, IncorrectArgumentType, RequiredArgumentNotFound }; -const CliError = error{ InvalidArg, InvalidNumberOfArgs, CliArgumentNotFound, InvalidCommand }; +const ArgMetadata = struct { + key: []const u8, + value: []const u8, + typ: std.builtin.Type, + optional: bool, +}; /// Snek - the primary CLI interface returning the anonnymous struct type for serializing all CLI arguments pub fn Snek(comptime CliInterface: type) type { return struct { allocator: std.mem.Allocator, + arg_metadata: std.StringHashMap(ArgMetadata), // Using @This() allows for destructuring of whatever this type is (i.e. allowing for metadata parsing of the cli struct) const Self = @This(); @@ -27,6 +27,7 @@ pub fn Snek(comptime CliInterface: type) type { pub fn init(allocator: std.mem.Allocator) !Self { return .{ .allocator = allocator, + .arg_metadata = std.StringHashMap(ArgMetadata).init(allocator), }; } @@ -85,14 +86,17 @@ pub fn Snek(comptime CliInterface: type) type { } } - /// Deinitializes memory - Caller is responsible for this pub fn deinit(self: *Self) void { _ = self; } - /// Primary CLI parser for pasing the incoming args from stdin and the incoming CliInterface to parse out the individual commands/fields - /// The return is the struct that is passed. Must be a struct - pub fn parse(self: *Self) !CliInterface { + /// deinitMem deinitializes abitrary memory + pub fn deinitMem(self: *Self, mem: anytype) void { + self.allocator.free(mem); + } + + /// Primary CLI parser for parsing the incoming args from stdin and the incoming CliInterface to parse out the individual commands/fields + pub fn parse(self: *Self) CliError!CliInterface { if (!self.isStruct()) { std.debug.print("Struct must be passed to parse function. Type: {any} found", .{@TypeOf(CliInterface)}); return; @@ -100,18 +104,77 @@ pub fn Snek(comptime CliInterface: type) type { const interface: CliInterface = undefined; - // Help is easy, we do not need to set nor care about the value const cli_reflected = @typeInfo(@TypeOf(interface)); - _ = cli_reflected; + + // Collect and do some initial checking on passed in flags/values + self.collectArgs() catch |e| { + switch (e) { + CliError.HelpCommand => { + self.help(); + + return e; + }, + CliError.InvalidCommand => { + std.debug.print("Invalid cli command was passed. Please use -help or -h to check help menu for available commands", .{}); + + return e; + }, + else => { + return e; + }, + } + }; + + unwrap_for: inline for (cli_reflected.Struct.fields) |field| { + const arg = self.arg_metadata.get(field.name); + + // If arg does NOT exist and the field is NOT optional, its an error case, so handle accordingly + if (!arg) { + switch (@typeInfo(field.type)) { + .Optional => { + continue :unwrap_for; + }, + else => { + std.debug.print("Required arugment {s} was not found in CLI flags. Check -help menu for required flags", .{field.name}); + return CliError.RequiredArgumentNotFound; + }, + } + } + + // Write data to struct field based on typ witin arg. Arg, at this point, should never be null + const serialized_arg = arg.?; + switch (@typeInfo(serialized_arg.typ)) { + .Bool => { + @field(&cli_reflected, serialized_arg.key) = try self.parseBool(serialized_arg.key); + }, + .Int => { + @field(&cli_reflected, serialized_arg.key) = try self.parseBool(serialized_arg.key); + }, + .Float => { + @field(&cli_reflected, serialized_arg.key) = try self.parseBool(serialized_arg.key); + }, + .Pointer => { + // .Pointer is for strings since the underlying type is []const u8 which is a .Pointer type + if (serialized_arg.typ.Pointer.size == .Slice and serialized_arg.typ.Pointer.child == u8) {} + }, + .Struct => {}, + else => { + @panic("unexpected type received in CLI parser"); + }, + } + } + + // Check that all struct fields are set and we are not missing any required fields. If we are, error. + // Track which fields are missing as well so we can reflect that back to the user + try self.checkAllStructArgs(cli_reflected); } // ## Helper Functions ## - // Get args from stdin fn collectArgs(self: *Self) !void { var args = try std.process.argsWithAllocator(self.allocator); defer deinit(self); - defer self.flushCliArgMap(); + // Skip first line, its always the name of the calling function _ = args.skip(); @@ -120,17 +183,67 @@ pub fn Snek(comptime CliInterface: type) type { return CliError.InvalidCommand; } - const split_arg = std.mem.split(u8, arg, "="); + // Remove the - without calling std.mem + const arg_stripped = arg[1..]; + + // Help command is treated as an exit case to display the help menu. This is the same way that Go does it in Flags + // https://cs.opensource.google/go/go/+/refs/tags/go1.23.1:src/flag/flag.go;l=1111 + if (std.mem.eql(arg_stripped, "help") or std.mem.eql(u8, arg_stripped, "h")) return CliError.HelpCommand; + + // Split on all data *after* the initial - and curate a roster of key/value arguments seerated by the = + const split_arg = std.mem.split(u8, arg_stripped, "="); const arg_key = split_arg.next() orelse ""; const arg_val = split_arg.next() orelse ""; + // Dupe memory to avoid issues with string pointer storage const arg_key_d = try self.allocator.dupe(u8, arg_key); const arg_val_d = try self.allocator.dupe(u8, arg_val); - _ = arg_key_d; - _ = arg_val_d; + // Check dedup map, if it is true, it was already found, skip adding and key check + if (self.arg_metadata.get(arg_key_d)) { + std.debug.print("Warn: Duplicate key {s} passed. Using previous argument!", .{arg_key_d}); + + continue; + } - // Now do all the parsing with the struct to insert the value + // No struct field of this name was found. Send error instead of moving on + if (!self.checkForKey(arg_key_d)) CliError.InvalidCommand; + + // .typ is used to eventually switch when we marshal the type of the value into the struct field + try self.arg_metadata.put(arg_key_d, .{ .key = arg_key_d, .value = arg_val_d, .optional = self.isOptional(arg_key_d), .typ = extractTypeInfoFromKey(arg_key_d) }); + } + } + + fn checkForKey(self: *Self, key: []const u8) bool { + _ = self; + return @hasField(CliInterface, key); + } + + /// Check all fields of the struct to ensure that all non-optional values are set and non are missing + fn checkAllStructArgs(self: *Self, comptime T: type) CliError!void { + _ = self; + _ = T; + } + + fn extractTypeInfoFromKey(key: []const u8) std.builtin.Type { + const s_enum = std.meta.stringToEnum(CliInterface, key); + + // We assume that the field is already found since it passed the hasKey check. So we do *not* handle the null case. + const field_info = std.meta.fieldInfo(CliInterface, s_enum.?); + + return @typeInfo(field_info); + } + + fn isOptional(self: *Self, key: []const u8) bool { + _ = self; + + switch (extractTypeInfoFromKey(key)) { + .Optional => { + return true; + }, + else => { + return false; + }, } } @@ -143,15 +256,55 @@ pub fn Snek(comptime CliInterface: type) type { return true; } - fn parseStdinArgs() !void {} + // ## Parser Functions ## + fn parseBool(self: Self, parse_value: []const u8) !void { + _ = self; + _ = parse_value; + } - fn parseCliInterface() !void {} + fn parseNumeric(self: Self, parse_value: []const u8) !void { + _ = self; + _ = parse_value; + } + + fn parseString(self: Self, parse_value: []const u8) !void { + _ = self; + _ = parse_value; + } }; } -test "Test struct with optional fields" {} +test "Test struct with optional fields" { + const T = struct { + test_one: ?[]const u8, + test_two: ?u32, + test_three: ?f64, + }; + + var snek = try Snek(T).init(std.heap.page_allocator); + _ = try snek.parse(); +} + +test "test struct with default fields" { + // Obviously stdin arguments are initially all strings. The datatype used in the struct will be used for the coercion of the type during parsing. + // If the coercian step fails to parse the respective value, an error will commence. + // This will display the help menu to the user + // const test_args = [_][]const u8{ "test", "123", "3.14" }; -test "test struct with default fields" {} + const T = struct { + default_string: []const u8 = "", + default_int: u32 = 420, + optional_value: ?f64, + default_bool: bool = true, // Technically the bool would have a false default generically due to the nature of bools, but non the less we can either specify or override. + }; + + var snek = try Snek(T).init(std.heap.page_allocator); + _ = try snek.parse(); + + // Validate that hte given default values will not change after parsing unless they are present in the stdin arguments. + + // Validate that the values will change to the incoming stdin arguments +} test "test struct with both optionals and defaults fields" {}