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