diff --git a/README.md b/README.md index 4fdcf39..ead0d08 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,32 @@ graph TD ## Run ```sh -zig build run +Usage: btczee [command] [args] + +Commands: + node + wallet +``` + +### Node + +```sh +Usage: btczee node + +Subcommands: + run Run the node + help Display help for node +``` + +### Wallet + +```sh +Usage: btczee wallet + +Subcommands: + create Create a new wallet + load Load an existing wallet + help Display help for wallet ``` ## Test diff --git a/build.zig b/build.zig index 7898f90..1d21137 100644 --- a/build.zig +++ b/build.zig @@ -6,8 +6,8 @@ const package_path = "src/lib.zig"; // List of external dependencies that this package requires. const external_dependencies = [_]build_helpers.Dependency{ .{ - .name = "zig-cli", - .module_name = "zig-cli", + .name = "clap", + .module_name = "clap", }, .{ .name = "httpz", diff --git a/build.zig.zon b/build.zig.zon index 658294e..beccc08 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,14 +6,14 @@ .url = "https://github.com/karlseguin/zul/archive/ae0c27350c0db6b460f22cba30b6b0c4a02d1ffd.zip", .hash = "1220457e2c8867f6734520d9b335f01e1d851d6fe7adaa7f6f0756158acaf6c5e87f", }, - .@"zig-cli" = .{ - .url = "https://github.com/sam701/zig-cli/archive/9a94c4803a52e54c26b198096d63fb5bde752da2.zip", - .hash = "1220ab73fb7cc11b2308edc3364988e05efcddbcac31b707f55e6216d1b9c0da13f1", - }, .httpz = .{ .url = "git+https://github.com/karlseguin/http.zig#d3e3fb3cf2f3caa2432338282dbe750f85e24254", .hash = "12205748a52926d9e6dbb1ac07462d8772c1e2fd6f52e4d822d8bf282f425efd1330", }, + .clap = .{ + .url = "git+https://github.com/Hejsil/zig-clap#2d9db156ae928860a9acf2f1260750d3b44a4c98", + .hash = "122005e589ab3b6bff8e589b45f5b12cd27ce79f266bdac17e9f33ebfe2fbaff7fe3", + }, }, .paths = .{ "build.zig", diff --git a/src/main.zig b/src/main.zig index a644358..204fb89 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,30 +5,147 @@ const Storage = @import("storage/storage.zig").Storage; const P2P = @import("network/p2p.zig").P2P; const RPC = @import("network/rpc.zig").RPC; const node = @import("node/node.zig"); +const ArgParser = @import("util/ArgParser.zig"); pub fn main() !void { // Initialize the allocator - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); + var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; + const gpa = gpa_state.allocator(); + defer _ = gpa_state.deinit(); + const args = try std.process.argsAlloc(gpa); + defer std.process.argsFree(gpa, args); + + var stdout_buffered = std.io.bufferedWriter(std.io.getStdOut().writer()); + const stdout = stdout_buffered.writer(); + + try mainFull(.{ + .allocator = gpa, + .args = args[1..], + .stdout = stdout.any(), + }); + + return stdout_buffered.flush(); +} + +pub fn mainFull(options: struct { + allocator: std.mem.Allocator, + args: []const []const u8, + stdout: std.io.AnyWriter, +}) !void { + var program = Program{ + .allocator = options.allocator, + .args = .{ .args = options.args }, + .stdout = options.stdout, + }; + + return program.mainCommand(); +} + +const Program = @This(); + +allocator: std.mem.Allocator, +args: ArgParser, +stdout: std.io.AnyWriter, + +const main_usage = + \\Usage: btczee [command] [args] + \\ + \\Commands: + \\ node + \\ wallet + \\ help Display this message + \\ +; + +pub fn mainCommand(program: *Program) !void { + while (program.args.next()) { + if (program.args.flag(&.{"node"})) + return program.nodeSubCommand(); + if (program.args.flag(&.{"wallet"})) + return program.walletSubCommand(); + if (program.args.flag(&.{ "-h", "--help", "help" })) + return program.stdout.writeAll(main_usage); + if (program.args.positional()) |_| { + try std.io.getStdErr().writeAll(main_usage); + return error.InvalidArgument; + } + } + try std.io.getStdErr().writeAll(main_usage); + return error.InvalidArgument; +} + +const node_sub_usage = + \\Usage: + \\ btczee node [command] [args] + \\ btczee node [options] [ids]... + \\ + \\Commands: + \\ help Display this message + \\ +; + +fn nodeSubCommand(program: *Program) !void { + // Handle potential node subcommands here + if (program.args.next()) { + if (program.args.flag(&.{ "-h", "--help", "help" })) + return program.stdout.writeAll(node_sub_usage); + } + + // Otherwise, run the node + return program.runNodeCommand(); +} + +fn runNodeCommand(program: *Program) !void { // Load configuration - var config = try Config.load(allocator, "bitcoin.conf.example"); + var config = try Config.load(program.allocator, "bitcoin.conf.example"); defer config.deinit(); // Initialize components - var mempool = try Mempool.init(allocator, &config); + var mempool = try Mempool.init(program.allocator, &config); defer mempool.deinit(); - var storage = try Storage.init(allocator, &config); + var storage = try Storage.init(program.allocator, &config); defer storage.deinit(); - var p2p = try P2P.init(allocator, &config); + var p2p = try P2P.init(program.allocator, &config); defer p2p.deinit(); - var rpc = try RPC.init(allocator, &config, &mempool, &storage); + var rpc = try RPC.init(program.allocator, &config, &mempool, &storage); defer rpc.deinit(); // Start the node try node.startNode(&mempool, &storage, &p2p, &rpc); } + +const wallet_sub_usage = + \\Usage: + \\ btczee wallet [command] [args] + \\ + \\Commands: + \\ create Create a new wallet + \\ load Load an existing wallet + \\ help Display this message + \\ +; + +fn walletSubCommand(program: *Program) !void { + if (program.args.next()) { + if (program.args.flag(&.{"create"})) + return program.walletCreateCommand(); + if (program.args.flag(&.{"load"})) + return program.walletLoadCommand(); + if (program.args.flag(&.{ "-h", "--help", "help" })) + return program.stdout.writeAll(wallet_sub_usage); + } + try std.io.getStdErr().writeAll(wallet_sub_usage); + return error.InvalidArgument; +} + +fn walletCreateCommand(program: *Program) !void { + return program.stdout.writeAll("Not implemented yet\n"); +} + +fn walletLoadCommand(program: *Program) !void { + return program.stdout.writeAll("Not implemented yet\n"); +} diff --git a/src/util/ArgParser.zig b/src/util/ArgParser.zig new file mode 100644 index 0000000..f9898f7 --- /dev/null +++ b/src/util/ArgParser.zig @@ -0,0 +1,186 @@ +/// Copied from: https://github.com/Hejsil/aniz/blob/master/src/ArgParser.zig +/// TODO: Move to using zig-clap as dependency +/// https://github.com/Hejsil/zig-clap +args: []const []const u8, +index: usize = 0, + +consumed: bool = false, + +pub fn next(parser: *ArgParser) bool { + parser.consumed = parser.index >= parser.args.len; + return !parser.consumed; +} + +pub fn flag(parser: *ArgParser, names: []const []const u8) bool { + if (parser.consumed) + return false; + + for (names) |name| { + if (!std.mem.eql(u8, parser.args[parser.index], name)) + continue; + + parser.consumed = true; + parser.index += 1; + return true; + } + + return false; +} + +pub fn option(parser: *ArgParser, names: []const []const u8) ?[]const u8 { + if (parser.consumed) + return null; + + const arg = parser.args[parser.index]; + for (names) |name| { + if (!std.mem.startsWith(u8, arg, name)) + continue; + if (!std.mem.startsWith(u8, arg[name.len..], "=")) + continue; + + parser.consumed = true; + parser.index += 1; + return arg[name.len + 1 ..]; + } + + if (parser.index + 1 < parser.args.len) { + if (parser.flag(names)) + return parser.eat(); + } + + return null; +} + +pub fn positional(parser: *ArgParser) ?[]const u8 { + if (parser.consumed) + return null; + + return parser.eat(); +} + +fn eat(parser: *ArgParser) []const u8 { + defer parser.index += 1; + return parser.args[parser.index]; +} + +test flag { + var parser = ArgParser{ .args = &.{ + "-a", "--beta", "command", + } }; + + try std.testing.expect(parser.flag(&.{ "-a", "--alpha" })); + try std.testing.expect(!parser.flag(&.{ "-b", "--beta" })); + try std.testing.expect(!parser.flag(&.{"command"})); + + try std.testing.expect(parser.next()); + try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); + try std.testing.expect(parser.flag(&.{ "-b", "--beta" })); + try std.testing.expect(!parser.flag(&.{"command"})); + + try std.testing.expect(parser.next()); + try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); + try std.testing.expect(!parser.flag(&.{ "-b", "--beta" })); + try std.testing.expect(parser.flag(&.{"command"})); + + try std.testing.expect(!parser.next()); + try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); + try std.testing.expect(!parser.flag(&.{ "-b", "--beta" })); + try std.testing.expect(!parser.flag(&.{"command"})); +} + +fn expectEqualOptionalString(m_expect: ?[]const u8, m_actual: ?[]const u8) !void { + if (m_expect) |expect| { + try std.testing.expect(m_actual != null); + try std.testing.expectEqualStrings(expect, m_actual.?); + } else { + try std.testing.expect(m_actual == null); + } +} + +test option { + var parser = ArgParser{ .args = &.{ + "-a", + "a_value", + "--beta=b_value", + "command", + "command_value", + } }; + + try expectEqualOptionalString("a_value", parser.option(&.{ "-a", "--alpha" })); + try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); + try expectEqualOptionalString(null, parser.option(&.{"command"})); + + try std.testing.expect(parser.next()); + try expectEqualOptionalString(null, parser.option(&.{ "-a", "--alpha" })); + try expectEqualOptionalString("b_value", parser.option(&.{ "-b", "--beta" })); + try expectEqualOptionalString(null, parser.option(&.{"command"})); + + try std.testing.expect(parser.next()); + try expectEqualOptionalString(null, parser.option(&.{ "-a", "--alpha" })); + try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); + try expectEqualOptionalString("command_value", parser.option(&.{"command"})); + + try std.testing.expect(!parser.next()); + try expectEqualOptionalString(null, parser.option(&.{ "-a", "--alpha" })); + try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); + try expectEqualOptionalString(null, parser.option(&.{"command"})); +} + +test positional { + var parser = ArgParser{ .args = &.{ + "-a", + "--beta", + "command", + } }; + + try expectEqualOptionalString("-a", parser.positional()); + try std.testing.expect(parser.next()); + try expectEqualOptionalString("--beta", parser.positional()); + try std.testing.expect(parser.next()); + try expectEqualOptionalString("command", parser.positional()); + try std.testing.expect(!parser.next()); + try expectEqualOptionalString(null, parser.positional()); +} + +test "all" { + var parser = ArgParser{ .args = &.{ + "-a", + "--beta", + "b_value", + "-c=c_value", + "command", + } }; + + try std.testing.expect(parser.flag(&.{ "-a", "--alpha" })); + try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); + try expectEqualOptionalString(null, parser.option(&.{ "-c", "--center" })); + try expectEqualOptionalString(null, parser.positional()); + + try std.testing.expect(parser.next()); + try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); + try expectEqualOptionalString("b_value", parser.option(&.{ "-b", "--beta" })); + try expectEqualOptionalString(null, parser.option(&.{ "-c", "--center" })); + try expectEqualOptionalString(null, parser.positional()); + + try std.testing.expect(parser.next()); + try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); + try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); + try expectEqualOptionalString("c_value", parser.option(&.{ "-c", "--center" })); + try expectEqualOptionalString(null, parser.positional()); + + try std.testing.expect(parser.next()); + try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); + try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); + try expectEqualOptionalString(null, parser.option(&.{ "-c", "--center" })); + try expectEqualOptionalString("command", parser.positional()); + + try std.testing.expect(!parser.next()); + try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); + try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); + try expectEqualOptionalString(null, parser.option(&.{ "-c", "--center" })); + try expectEqualOptionalString(null, parser.positional()); +} + +const ArgParser = @This(); + +const std = @import("std");