-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0c34cc0
commit 096efe1
Showing
9 changed files
with
369 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
name: Build and Test | ||
|
||
on: | ||
pull_request: | ||
branches: | ||
- main | ||
push: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
build-linux: | ||
name: Build & Test | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Install Zig | ||
run: "sudo snap install zig --classic --beta" | ||
|
||
- name: Build & Test | ||
run: zig build test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.zig-cache/ | ||
zig-out/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,70 @@ | ||
<div align="center"> | ||
|
||
<img src="/assets/logo.png" width="450" height="500"> | ||
</div> | ||
|
||
|
||
# snek | ||
Snek - A simple CLI parser to build CLI applications in Zig | ||
|
||
|
||
## Note: This is currently under construction and is not yet available for public consumption (hence no releases!) | ||
|
||
|
||
### Usage: | ||
Add snek to your zig project: | ||
``` | ||
zig fetch --save https://github.com/BitlyTwiser/snek/archive/refs/tags/0.1.0.tar.gz | ||
``` | ||
|
||
Add to build 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. | ||
|
||
``` | ||
const T = struct { | ||
bool_test: bool, | ||
word: []const u8, | ||
test_opt: ?u32, | ||
}; | ||
var snek = try Snek(T).init(std.heap.page_allocator); | ||
try snek.help(); | ||
``` | ||
|
||
#### Optionals: | ||
Using zig optionals, you can set selected flags to be ignored if they are not present. | ||
|
||
#### Default Values: | ||
You can use struct defaut values to set a static value if one is not parsed. | ||
|
||
|
||
### 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 { | ||
bool_test: bool, | ||
word: []const u8, | ||
test_opt: ?u32, | ||
}; | ||
var snek = try Snek(T).init(std.heap.page_allocator); | ||
try snek.parse(); | ||
// Print the values from the serialized struct data | ||
std.debug.print("{any}", .{T.bool_test}); | ||
``` | ||
Output: | ||
``` | ||
CLI Flags Help Menu | ||
--------- | ||
-bool_test=Bool (optional: false) | ||
-word=Pointer (optional: false) | ||
-test_opt=Optional (optional: true) | ||
--------- | ||
``` | ||
|
||
|
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
const std = @import("std"); | ||
|
||
pub fn build(b: *std.Build) void { | ||
// We build a binary for testing and the actual module for use | ||
const target = b.standardTargetOptions(.{}); | ||
|
||
const optimize = b.standardOptimizeOption(.{}); | ||
|
||
const exe = b.addExecutable(.{ | ||
.name = "snek", | ||
.root_source_file = b.path("src/main.zig"), | ||
.target = target, | ||
.optimize = optimize, | ||
}); | ||
|
||
// For setting env vars using C | ||
exe.linkLibC(); | ||
b.installArtifact(exe); | ||
|
||
// Module setup | ||
_ = b.addModule("snek", .{ .root_source_file = b.path("src/main.zig") }); | ||
var lib_tests = b.addTest(.{ | ||
.root_source_file = b.path("src/main.zig"), | ||
.optimize = optimize, | ||
}); | ||
|
||
const test_step = b.step("test", "Run library tests"); | ||
test_step.dependOn(&lib_tests.step); | ||
|
||
// Export the library module | ||
_ = b.addModule("snek", .{ | ||
.root_source_file = b.path("src/lib.zig"), | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
.{ | ||
// This is the default name used by packages depending on this one. For | ||
// example, when a user runs `zig fetch --save <url>`, this field is used | ||
// as the key in the `dependencies` table. Although the user can choose a | ||
// different name, most users will stick with this provided value. | ||
// | ||
// It is redundant to include "zig" in this name because it is already | ||
// within the Zig package namespace. | ||
.name = "snek", | ||
|
||
// This is a [Semantic Version](https://semver.org/). | ||
// In a future version of Zig it will be used for package deduplication. | ||
.version = "0.0.0", | ||
|
||
// This field is optional. | ||
// This is currently advisory only; Zig does not yet do anything | ||
// with this value. | ||
//.minimum_zig_version = "0.11.0", | ||
|
||
// This field is optional. | ||
// Each dependency must either provide a `url` and `hash`, or a `path`. | ||
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively. | ||
// Once all dependencies are fetched, `zig build` no longer requires | ||
// internet connectivity. | ||
.dependencies = .{ | ||
// See `zig fetch --save <url>` for a command-line interface for adding dependencies. | ||
//.example = .{ | ||
// // When updating this field to a new URL, be sure to delete the corresponding | ||
// // `hash`, otherwise you are communicating that you expect to find the old hash at | ||
// // the new URL. | ||
// .url = "https://example.com/foo.tar.gz", | ||
// | ||
// // This is computed from the file contents of the directory of files that is | ||
// // obtained after fetching `url` and applying the inclusion rules given by | ||
// // `paths`. | ||
// // | ||
// // This field is the source of truth; packages do not come from a `url`; they | ||
// // come from a `hash`. `url` is just one of many possible mirrors for how to | ||
// // obtain a package matching this `hash`. | ||
// // | ||
// // Uses the [multihash](https://multiformats.io/multihash/) format. | ||
// .hash = "...", | ||
// | ||
// // When this is provided, the package is found in a directory relative to the | ||
// // build root. In this case the package's hash is irrelevant and therefore not | ||
// // computed. This field and `url` are mutually exclusive. | ||
// .path = "foo", | ||
|
||
// // When this is set to `true`, a package is declared to be lazily | ||
// // fetched. This makes the dependency only get fetched if it is | ||
// // actually used. | ||
// .lazy = false, | ||
//}, | ||
}, | ||
|
||
// Specifies the set of files and directories that are included in this package. | ||
// Only files and directories listed here are included in the `hash` that | ||
// is computed for this package. Only files listed here will remain on disk | ||
// when using the zig package manager. As a rule of thumb, one should list | ||
// files required for compilation plus any license(s). | ||
// Paths are relative to the build root. Use the empty string (`""`) to refer to | ||
// the build root itself. | ||
// A directory listed here means that all files within, recursively, are included. | ||
.paths = .{ | ||
"build.zig", | ||
"build.zig.zon", | ||
"src", | ||
// For example... | ||
//"LICENSE", | ||
//"README.md", | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
const snek = @import("sneaky.zig"); | ||
const Snek = snek.Snek; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const std = @import("std"); | ||
|
||
pub fn main() !void {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
// Sneaky Snek | ||
|
||
const std = @import("std"); | ||
const builtin = @import("builtin"); | ||
|
||
// 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 | ||
|
||
const CliError = error{ InvalidArg, InvalidNumberOfArgs, CliArgumentNotFound, InvalidCommand }; | ||
|
||
/// 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, | ||
// Using @This() allows for destructuring of whatever this type is (i.e. allowing for metadata parsing of the cli struct) | ||
const Self = @This(); | ||
|
||
// ## Public API Functions ## | ||
|
||
/// General Init function for curating CliInterface type | ||
pub fn init(allocator: std.mem.Allocator) !Self { | ||
return .{ | ||
.allocator = allocator, | ||
}; | ||
} | ||
|
||
/// Help - Prints out the fields and their expected types to the user for a simple help menu for the CLI | ||
pub fn help(self: *Self) !void { | ||
if (!self.isStruct()) { | ||
std.debug.print("A valid struct must be passed to help function. Type: {any} found", .{@TypeOf(CliInterface)}); | ||
return; | ||
} | ||
|
||
const interface: CliInterface = undefined; | ||
|
||
// Help is easy, we do not need to set nor care about the value | ||
const cli_reflected = @typeInfo(@TypeOf(interface)); | ||
|
||
if (cli_reflected == .Struct) { | ||
const field_size = cli_reflected.Struct.fields.len; | ||
|
||
var command_slice = try self.allocator.alloc([]u8, field_size); | ||
|
||
var index: usize = 0; | ||
inline for (cli_reflected.Struct.fields) |field| { | ||
comptime var field_type: std.builtin.Type = undefined; | ||
var optional_field: bool = false; // Assumed false until proven otherwise | ||
|
||
const field_name = field.name; | ||
|
||
const temp_field_type = @typeInfo(field.type); | ||
// What is the type of the optional field | ||
switch (temp_field_type) { | ||
.Optional => { | ||
optional_field = true; | ||
field_type = temp_field_type; | ||
}, | ||
else => { | ||
field_type = temp_field_type; | ||
}, | ||
} | ||
|
||
const command_string = try std.fmt.allocPrint(self.allocator, "-{s}={s} (optional: {any})\n", .{ field_name, @tagName(field_type), optional_field }); | ||
command_slice[index] = command_string; | ||
|
||
index += 1; | ||
} | ||
|
||
// Iterate and print commands in small help menu | ||
std.debug.print("CLI Flags Help Menu\n", .{}); | ||
std.debug.print("---------\n", .{}); | ||
for (command_slice) |command| { | ||
std.debug.print("{s}", .{command}); | ||
} | ||
std.debug.print("---------\n", .{}); | ||
} else { | ||
std.debug.print("A valid struct was not passed into snek. Please ensure struct is valid and try again", .{}); | ||
return; | ||
} | ||
} | ||
|
||
/// 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 { | ||
if (!self.isStruct()) { | ||
std.debug.print("Struct must be passed to parse function. Type: {any} found", .{@TypeOf(CliInterface)}); | ||
return; | ||
} | ||
|
||
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; | ||
} | ||
|
||
// ## 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(); | ||
|
||
while (args.next()) |arg| { | ||
if (arg[0] != '-') { | ||
return CliError.InvalidCommand; | ||
} | ||
|
||
const split_arg = std.mem.split(u8, arg, "="); | ||
const arg_key = split_arg.next() orelse ""; | ||
const arg_val = split_arg.next() orelse ""; | ||
|
||
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; | ||
|
||
// Now do all the parsing with the struct to insert the value | ||
} | ||
} | ||
|
||
// Ensures passed in value is a struct. It cannot be anything else so strict checking is applied to public functions | ||
fn isStruct(self: *Self) bool { | ||
_ = self; | ||
const cli_reflected = @typeInfo(CliInterface); | ||
if (cli_reflected != .Struct) return false; | ||
|
||
return true; | ||
} | ||
|
||
fn parseStdinArgs() !void {} | ||
|
||
fn parseCliInterface() !void {} | ||
}; | ||
} | ||
|
||
test "Test struct with optional fields" {} | ||
|
||
test "test struct with default fields" {} | ||
|
||
test "test struct with both optionals and defaults fields" {} | ||
|
||
test "test struct with all general fields" { | ||
const T = struct { | ||
bool_test: bool, | ||
word: []const u8, | ||
test_opt: ?u32, | ||
}; | ||
var snek = try Snek(T).init(std.heap.page_allocator); | ||
try snek.help(); | ||
} |