╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ command.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
2 const srv = @import("./serve.zig"); 3 4 pub const CommandKind = enum { 5 serve, 6 }; 7 8 const Help = struct { 9 name: []const u8, 10 descrip: []const u8, 11 example: []const u8, 12 }; 13 14 fn commandHelp(command_kind: CommandKind) Help { 15 return switch (command_kind) { 16 .serve => .{ 17 .name = "serve", 18 .descrip = 19 \\a long-running server forwarding receive-pack and upload-pack. 20 , 21 .example = 22 \\haxy serve --http-listen 127.0.0.1:8080 --ssh-listen 127.0.0.1:8022 --wui-listen 127.0.0.1:8000 --data-dir /srv/git 23 \\use "none" for --http-listen or --ssh-listen to disable that service. 24 , 25 }, 26 }; 27 } 28 29 pub fn printHelp(cmd_kind_maybe: ?CommandKind, writer: *std.Io.Writer) !void { 30 const print_indent = comptime blk: { 31 var indent = 0; 32 for (0..@typeInfo(CommandKind).@"enum".field_names.len) |i| { 33 indent = @max(commandHelp(@fromBackingInt(@intCast(i))).name.len, indent); 34 } 35 indent += 2; 36 break :blk indent; 37 }; 38 39 if (cmd_kind_maybe) |cmd_kind| { 40 const help = commandHelp(cmd_kind); 41 // name and description 42 try writer.print("{s}", .{help.name}); 43 for (0..print_indent - help.name.len) |_| try writer.print(" ", .{}); 44 var split_iter = std.mem.splitScalar(u8, help.descrip, '\n'); 45 try writer.print("{s}\n", .{split_iter.first()}); 46 while (split_iter.next()) |line| { 47 for (0..print_indent) |_| try writer.print(" ", .{}); 48 try writer.print("{s}\n", .{line}); 49 } 50 try writer.print("\n", .{}); 51 // example 52 split_iter = std.mem.splitScalar(u8, help.example, '\n'); 53 while (split_iter.next()) |line| { 54 for (0..print_indent) |_| try writer.print(" ", .{}); 55 try writer.print("{s}\n", .{line}); 56 } 57 } else { 58 try writer.print("help: haxy <command> [<args>]\n\n", .{}); 59 inline for (@typeInfo(CommandKind).@"enum".field_values) |value| { 60 const help = commandHelp(@fromBackingInt(@intCast(value))); 61 // name and description 62 try writer.print("{s}", .{help.name}); 63 for (0..print_indent - help.name.len) |_| try writer.print(" ", .{}); 64 var split_iter = std.mem.splitScalar(u8, help.descrip, '\n'); 65 try writer.print("{s}\n", .{split_iter.first()}); 66 while (split_iter.next()) |line| { 67 for (0..print_indent) |_| try writer.print(" ", .{}); 68 try writer.print("{s}\n", .{line}); 69 } 70 } 71 } 72 } 73 74 pub const CommandArgs = struct { 75 allocator: std.mem.Allocator, 76 arena: *std.heap.ArenaAllocator, 77 command_kind: ?CommandKind, 78 command_name: ?[]const u8, 79 positional_args: []const []const u8, 80 map_args: std.StringArrayHashMapUnmanaged(?[]const u8), 81 unused_args: std.StringArrayHashMapUnmanaged(void), 82 83 // flags that can have a value associated with them 84 // must be included here 85 const value_flags = std.StaticStringMap(void).initComptime(.{ 86 .{"--http-listen"}, 87 .{"--ssh-listen"}, 88 .{"--wui-listen"}, 89 .{"--data-dir"}, 90 }); 91 92 pub fn init(allocator: std.mem.Allocator, args: []const []const u8) !CommandArgs { 93 const arena = try allocator.create(std.heap.ArenaAllocator); 94 arena.* = std.heap.ArenaAllocator.init(allocator); 95 errdefer { 96 arena.deinit(); 97 allocator.destroy(arena); 98 } 99 100 var positional_args: std.ArrayList([]const u8) = .empty; 101 var map_args: std.StringArrayHashMapUnmanaged(?[]const u8) = .empty; 102 var unused_args: std.StringArrayHashMapUnmanaged(void) = .empty; 103 104 for (args) |arg| { 105 if (arg.len > 1 and arg[0] == '-') { 106 try map_args.put(arena.allocator(), arg, null); 107 try unused_args.put(arena.allocator(), arg, {}); 108 } else { 109 // if the last key is a value flag and doesn't have a value yet, 110 // set this arg as its value 111 const keys = map_args.keys(); 112 if (keys.len > 0) { 113 const last_key = keys[keys.len - 1]; 114 if (map_args.get(last_key)) |last_val| { 115 if (value_flags.has(last_key) and last_val == null) { 116 try map_args.put(arena.allocator(), last_key, arg); 117 continue; 118 } 119 } 120 } 121 122 // in any other case, just consider it a positional arg 123 try positional_args.append(arena.allocator(), arg); 124 } 125 } 126 127 const args_slice = try positional_args.toOwnedSlice(arena.allocator()); 128 if (args_slice.len == 0) { 129 return .{ 130 .allocator = allocator, 131 .arena = arena, 132 .command_kind = null, 133 .command_name = null, 134 .positional_args = args_slice, 135 .map_args = map_args, 136 .unused_args = unused_args, 137 }; 138 } else { 139 const command_name = args_slice[0]; 140 const extra_args = args_slice[1..]; 141 142 const command_kind: ?CommandKind = inline for (0..@typeInfo(CommandKind).@"enum".field_names.len) |i| { 143 if (std.mem.eql(u8, command_name, commandHelp(@fromBackingInt(@intCast(i))).name)) { 144 break @fromBackingInt(@intCast(i)); 145 } 146 } else null; 147 148 return .{ 149 .allocator = allocator, 150 .arena = arena, 151 .command_kind = command_kind, 152 .command_name = command_name, 153 .positional_args = extra_args, 154 .map_args = map_args, 155 .unused_args = unused_args, 156 }; 157 } 158 } 159 160 pub fn deinit(self: *CommandArgs) void { 161 self.arena.deinit(); 162 self.allocator.destroy(self.arena); 163 } 164 165 pub fn contains(self: *CommandArgs, arg: []const u8) bool { 166 _ = self.unused_args.orderedRemove(arg); 167 return self.map_args.contains(arg); 168 } 169 170 pub fn get(self: *CommandArgs, comptime arg: []const u8) ??[]const u8 { 171 comptime std.debug.assert(value_flags.has(arg)); // can only call `get` with flags included in `value_flags` 172 _ = self.unused_args.orderedRemove(arg); 173 return self.map_args.get(arg); 174 } 175 }; 176 177 /// parses the args into a format that can be directly used by a repo. 178 /// if any additional allocation needs to be done, the arena inside the cmd args will be used. 179 pub const Command = union(CommandKind) { 180 serve: srv.Options, 181 182 pub fn initMaybe(cmd_args: *CommandArgs) !?Command { 183 const command_kind = cmd_args.command_kind orelse return null; 184 switch (command_kind) { 185 .serve => { 186 return .{ .serve = try serveOptions(cmd_args) orelse return null }; 187 }, 188 } 189 } 190 }; 191 192 pub fn parseServeOptions(allocator: std.mem.Allocator, args: []const []const u8) !srv.Options { 193 var cmd_args = try CommandArgs.init(allocator, args); 194 defer cmd_args.deinit(); 195 const options = try serveOptions(&cmd_args) orelse return error.InvalidServeArgument; 196 if (cmd_args.unused_args.count() != 0) return error.InvalidServeArgument; 197 return options; 198 } 199 200 fn serveOptions(cmd_args: *CommandArgs) !?srv.Options { 201 if (cmd_args.positional_args.len != 0) return null; 202 203 var options: srv.Options = .{}; 204 if (cmd_args.get("--http-listen")) |value_maybe| { 205 const value = value_maybe orelse return error.HttpListenNeedsValue; 206 options.http_listen = if (std.mem.eql(u8, value, "none")) null else value; 207 } 208 if (cmd_args.get("--ssh-listen")) |value_maybe| { 209 const value = value_maybe orelse return error.SshListenNeedsValue; 210 options.ssh_listen = if (std.mem.eql(u8, value, "none")) null else value; 211 } 212 if (cmd_args.get("--wui-listen")) |value_maybe| options.wui_listen = value_maybe orelse return error.WuiListenNeedsValue; 213 if (cmd_args.get("--data-dir")) |value_maybe| options.data_dir = value_maybe orelse return error.DataDirNeedsValue; 214 return options; 215 } 216 217 /// parses the given args into a command if valid, and determines how it should be run 218 /// (via the TUI or CLI). 219 pub const CommandDispatch = union(enum) { 220 invalid: union(enum) { 221 command: []const u8, 222 argument: struct { 223 command: ?CommandKind, 224 value: []const u8, 225 }, 226 }, 227 help: ?CommandKind, 228 local, 229 cli: Command, 230 231 pub fn init(cmd_args: *CommandArgs) !CommandDispatch { 232 const dispatch = try initIgnoreUnused(cmd_args); 233 if (cmd_args.unused_args.count() > 0) { 234 return .{ 235 .invalid = .{ 236 .argument = .{ 237 .command = switch (dispatch) { 238 .invalid => return dispatch, // if there was already an error, return it instead 239 .help => |cmd_kind_maybe| cmd_kind_maybe, 240 .local => null, 241 .cli => |command| command, 242 }, 243 .value = cmd_args.unused_args.keys()[0], 244 }, 245 }, 246 }; 247 } 248 return dispatch; 249 } 250 251 pub fn initIgnoreUnused(cmd_args: *CommandArgs) !CommandDispatch { 252 const show_help = cmd_args.contains("--help"); 253 254 if (cmd_args.command_kind) |command_kind| { 255 if (show_help) { 256 return .{ .help = command_kind }; 257 } else if (try Command.initMaybe(cmd_args)) |cmd| { 258 return .{ .cli = cmd }; 259 } else { 260 return .{ .help = command_kind }; 261 } 262 } else if (cmd_args.command_name) |command_name| { 263 return .{ .invalid = .{ .command = command_name } }; 264 } else if (show_help) { 265 return .{ .help = null }; 266 } else { 267 return .local; 268 } 269 } 270 }; 271