╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ command.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
77 arena: *std.heap.ArenaAllocator, 78 command_kind: ?CommandKind, 79 command_name: ?[]const u8, 80 positional_args: []const []const u8, 81 map_args: std.StringArrayHashMapUnmanaged(?[]const u8), 82 unused_args: std.StringArrayHashMapUnmanaged(void), 83 84 // flags that can have a value associated with them 85 // must be included here 86 const value_flags = std.StaticStringMap(void).initComptime(.{ 87 .{"--web-listen"}, 88 .{"--ssh-listen"}, 89 .{"--web-url"}, 90 .{"--ssh-url"}, 91 .{"--data-dir"}, 92 }); 93 94 pub fn init(allocator: std.mem.Allocator, args: []const []const u8) !CommandArgs { 95 const arena = try allocator.create(std.heap.ArenaAllocator); 96 arena.* = std.heap.ArenaAllocator.init(allocator); 97 errdefer { 98 arena.deinit(); 99 allocator.destroy(arena); 100 } 101 102 var positional_args: std.ArrayList([]const u8) = .empty; 103 var map_args: std.StringArrayHashMapUnmanaged(?[]const u8) = .empty; 104 var unused_args: std.StringArrayHashMapUnmanaged(void) = .empty; 105 106 for (args) |arg| { 107 if (arg.len > 1 and arg[0] == '-') { 108 try map_args.put(arena.allocator(), arg, null); 109 try unused_args.put(arena.allocator(), arg, {}); 110 } else { 111 // if the last key is a value flag and doesn't have a value yet, 112 // set this arg as its value 113 const keys = map_args.keys(); 114 if (keys.len > 0) { 115 const last_key = keys[keys.len - 1]; 116 if (map_args.get(last_key)) |last_val| { 117 if (value_flags.has(last_key) and last_val == null) { 118 try map_args.put(arena.allocator(), last_key, arg); 119 continue; 120 } 121 } 122 } 123 124 // in any other case, just consider it a positional arg 125 try positional_args.append(arena.allocator(), arg); 126 } 127 } 128 129 const args_slice = try positional_args.toOwnedSlice(arena.allocator()); 130 if (args_slice.len == 0) { 131 return .{ 132 .allocator = allocator, 133 .arena = arena, 134 .command_kind = null, 135 .command_name = null, 136 .positional_args = args_slice, 137 .map_args = map_args, 138 .unused_args = unused_args, 139 }; 140 } else { 141 const command_name = args_slice[0]; 142 const extra_args = args_slice[1..]; 143 144 const command_kind: ?CommandKind = inline for (0..@typeInfo(CommandKind).@"enum".field_names.len) |i| { 145 if (std.mem.eql(u8, command_name, commandHelp(@fromBackingInt(@intCast(i))).name)) { 146 break @fromBackingInt(@intCast(i)); 147 } 148 } else null; 149 150 return .{ 151 .allocator = allocator, 152 .arena = arena, 153 .command_kind = command_kind, 154 .command_name = command_name, 155 .positional_args = extra_args, 156 .map_args = map_args, 157 .unused_args = unused_args, 158 }; 159 } 160 } 161 162 pub fn deinit(self: *CommandArgs) void { 163 self.arena.deinit(); 164 self.allocator.destroy(self.arena); 165 } 166 167 pub fn contains(self: *CommandArgs, arg: []const u8) bool { 168 _ = self.unused_args.orderedRemove(arg); 169 return self.map_args.contains(arg); 170 } 171 172 pub fn get(self: *CommandArgs, comptime arg: []const u8) ??[]const u8 { 173 comptime std.debug.assert(value_flags.has(arg)); // can only call `get` with flags included in `value_flags` 174 _ = self.unused_args.orderedRemove(arg); 175 return self.map_args.get(arg); 176 } 177 }; 178 179 /// parses the args into a format that can be directly used by a repo. 180 /// if any additional allocation needs to be done, the arena inside the cmd args will be used. 181 pub const Command = union(CommandKind) { 182 serve: srv.Options, 183 184 pub fn initMaybe(cmd_args: *CommandArgs) !?Command { 185 const command_kind = cmd_args.command_kind orelse return null; 186 switch (command_kind) { 187 .serve => { 188 return .{ .serve = try serveOptions(cmd_args) orelse return null }; 189 }, 190 } 191 } 192 }; 193 194 pub fn parseServeOptions(allocator: std.mem.Allocator, args: []const []const u8) !srv.Options { 195 var cmd_args = try CommandArgs.init(allocator, args); 196 defer cmd_args.deinit(); 197 const options = try serveOptions(&cmd_args) orelse return error.InvalidServeArgument; 198 if (cmd_args.unused_args.count() != 0) return error.InvalidServeArgument; 199 return options; 200 } 201 202 fn serveOptions(cmd_args: *CommandArgs) !?srv.Options { 203 if (cmd_args.positional_args.len != 0) return null; 204 205 var options: srv.Options = .{}; 206 if (cmd_args.get("--web-listen")) |value_maybe| options.web_listen = value_maybe orelse return error.WebListenNeedsValue; 207 if (cmd_args.get("--ssh-listen")) |value_maybe| { 208 const value = value_maybe orelse return error.SshListenNeedsValue; 209 options.ssh_listen = if (std.mem.eql(u8, value, "none")) null else value; 210 } 211 // urls are joined with "/" + a path 212 if (cmd_args.get("--web-url")) |value_maybe| options.web_url = std.mem.trimEnd(u8, value_maybe orelse return error.WebUrlNeedsValue, "/"); 213 if (cmd_args.get("--ssh-url")) |value_maybe| options.ssh_url = std.mem.trimEnd(u8, value_maybe orelse return error.SshUrlNeedsValue, "/"); 214 if (cmd_args.get("--data-dir")) |value_maybe| options.data_dir = value_maybe orelse return error.DataDirNeedsValue; 215 return options; 216 } 217 218 /// parses the given args into a command if valid, and determines how it should be run 219 /// (via the TUI or CLI). 220 pub const CommandDispatch = union(enum) { 221 invalid: union(enum) { 222 command: []const u8, 223 argument: struct { 224 command: ?CommandKind, 225 value: []const u8, 226 }, 227 }, 228 help: ?CommandKind, 229 local, 230 cli: Command, 231 232 pub fn init(cmd_args: *CommandArgs) !CommandDispatch { 233 const dispatch = try initIgnoreUnused(cmd_args); 234 if (cmd_args.unused_args.count() > 0) { 235 return .{ 236 .invalid = .{ 237 .argument = .{ 238 .command = switch (dispatch) { 239 .invalid => return dispatch, // if there was already an error, return it instead 240 .help => |cmd_kind_maybe| cmd_kind_maybe, 241 .local => null, 242 .cli => |command| command, 243 }, 244 .value = cmd_args.unused_args.keys()[0], 245 }, 246 }, 247 }; 248 } 249 return dispatch; 250 } 251 252 pub fn initIgnoreUnused(cmd_args: *CommandArgs) !CommandDispatch { 253 const show_help = cmd_args.contains("--help"); 254 255 if (cmd_args.command_kind) |command_kind| { 256 if (show_help) { 257 return .{ .help = command_kind }; 258 } else if (try Command.initMaybe(cmd_args)) |cmd| { 259 return .{ .cli = cmd }; 260 } else { 261 return .{ .help = command_kind }; 262 } 263 } else if (cmd_args.command_name) |command_name| { 264 return .{ .invalid = .{ .command = command_name } }; 265 } else if (show_help) { 266 return .{ .help = null }; 267 } else { 268 return .local; 269 } 270 } 271 }; 272