Skip to content

Commit

Permalink
Initial (and early) push of snek
Browse files Browse the repository at this point in the history
  • Loading branch information
BitlyTwiser committed Sep 21, 2024
1 parent 0c34cc0 commit 096efe1
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/build.yml
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.zig-cache/
zig-out/
68 changes: 68 additions & 0 deletions README.md
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)
---------
```


Binary file added assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions build.zig
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"),
});
}
72 changes: 72 additions & 0 deletions build.zig.zon
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",
},
}
2 changes: 2 additions & 0 deletions src/lib.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const snek = @import("sneaky.zig");
const Snek = snek.Snek;
3 changes: 3 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const std = @import("std");

pub fn main() !void {}
166 changes: 166 additions & 0 deletions src/sneaky.zig
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();
}

0 comments on commit 096efe1

Please sign in to comment.