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