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