diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | bits.zig | 9 | ||||
| -rw-r--r-- | clap.zig | 332 |
3 files changed, 342 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2040c29 --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1 @@ | |||
| zig-cache | |||
diff --git a/bits.zig b/bits.zig new file mode 100644 index 0000000..64f7d8b --- /dev/null +++ b/bits.zig | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | const math = @import("std").math; | ||
| 2 | |||
| 3 | pub fn set(comptime Int: type, num: Int, bit: math.Log2Int(Int), value: bool) Int { | ||
| 4 | return (num & ~(Int(1) << bit)) | (Int(value) << bit); | ||
| 5 | } | ||
| 6 | |||
| 7 | pub fn get(comptime Int: type, num: Int, bit: math.Log2Int(Int)) bool { | ||
| 8 | return ((num >> bit) & 1) != 0; | ||
| 9 | } | ||
diff --git a/clap.zig b/clap.zig new file mode 100644 index 0000000..c9a45d3 --- /dev/null +++ b/clap.zig | |||
| @@ -0,0 +1,332 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | const bits = @import("bits.zig"); | ||
| 3 | |||
| 4 | const mem = std.mem; | ||
| 5 | const fmt = std.fmt; | ||
| 6 | const debug = std.debug; | ||
| 7 | const io = std.io; | ||
| 8 | |||
| 9 | const assert = debug.assert; | ||
| 10 | |||
| 11 | // TODO: Missing a few convinient features | ||
| 12 | // * Short arguments that doesn't take values should probably be able to be | ||
| 13 | // chain like many linux programs: "rm -rf" | ||
| 14 | // * Handle "--something=VALUE" | ||
| 15 | pub fn Option(comptime Result: type, comptime ParseError: type) type { | ||
| 16 | return struct { | ||
| 17 | const Self = this; | ||
| 18 | |||
| 19 | pub const Kind = enum { | ||
| 20 | Optional, | ||
| 21 | Required, | ||
| 22 | IgnoresRequired | ||
| 23 | }; | ||
| 24 | |||
| 25 | parser: fn(&Result, []const u8) ParseError!void, | ||
| 26 | help: []const u8, | ||
| 27 | kind: Kind, | ||
| 28 | takes_value: bool, | ||
| 29 | short: ?u8, | ||
| 30 | long: ?[]const u8, | ||
| 31 | |||
| 32 | pub fn init(parser: fn(&Result, []const u8) ParseError!void) Self { | ||
| 33 | return Self { | ||
| 34 | .parser = parser, | ||
| 35 | .help = "", | ||
| 36 | .kind = Kind.Optional, | ||
| 37 | .takes_value = false, | ||
| 38 | .short = null, | ||
| 39 | .long = null, | ||
| 40 | }; | ||
| 41 | } | ||
| 42 | |||
| 43 | pub fn setHelp(option: &const Self, help_str: []const u8) Self { | ||
| 44 | var res = *option; res.help = help_str; | ||
| 45 | return res; | ||
| 46 | } | ||
| 47 | |||
| 48 | pub fn setKind(option: &const Self, kind: Kind) Self { | ||
| 49 | var res = *option; res.kind = kind; | ||
| 50 | return res; | ||
| 51 | } | ||
| 52 | |||
| 53 | pub fn takesValue(option: &const Self, takes_value: bool) Self { | ||
| 54 | var res = *option; res.takes_value = takes_value; | ||
| 55 | return res; | ||
| 56 | } | ||
| 57 | |||
| 58 | pub fn setShort(option: &const Self, short: u8) Self { | ||
| 59 | var res = *option; res.short = short; | ||
| 60 | return res; | ||
| 61 | } | ||
| 62 | |||
| 63 | pub fn setLong(option: &const Self, long: []const u8) Self { | ||
| 64 | var res = *option; res.long = long; | ||
| 65 | return res; | ||
| 66 | } | ||
| 67 | }; | ||
| 68 | } | ||
| 69 | |||
| 70 | pub fn Parser(comptime Result: type, comptime ParseError: type, comptime defaults: &const Result, | ||
| 71 | comptime options: []const Option(Result, ParseError)) type { | ||
| 72 | |||
| 73 | const OptionT = Option(Result, ParseError); | ||
| 74 | const Arg = struct { | ||
| 75 | const Kind = enum { Long, Short, None }; | ||
| 76 | |||
| 77 | arg: []const u8, | ||
| 78 | kind: Kind | ||
| 79 | }; | ||
| 80 | |||
| 81 | // NOTE: For now, a bitfield is used to keep track of the required arguments. | ||
| 82 | // This limits the user to 128 required arguments, which is more than | ||
| 83 | // enough. | ||
| 84 | const required_mask = comptime blk: { | ||
| 85 | var required_res : u128 = 0; | ||
| 86 | for (options) |option, i| { | ||
| 87 | if (option.kind == OptionT.Kind.Required) { | ||
| 88 | required_res = (required_res << 1) | 0x1; | ||
| 89 | } | ||
| 90 | } | ||
| 91 | |||
| 92 | break :blk required_res; | ||
| 93 | }; | ||
| 94 | |||
| 95 | return struct { | ||
| 96 | fn parse(args: []const []const u8) !Result { | ||
| 97 | var result = *defaults; | ||
| 98 | var required = required_mask; | ||
| 99 | |||
| 100 | var arg_i = usize(0); | ||
| 101 | loop: while (arg_i < args.len) : (arg_i += 1) { | ||
| 102 | const pair = blk: { | ||
| 103 | const tmp = args[arg_i]; | ||
| 104 | if (mem.startsWith(u8, tmp, "--")) | ||
| 105 | break :blk Arg { .arg = tmp[2..], .kind = Arg.Kind.Long }; | ||
| 106 | if (mem.startsWith(u8, tmp, "-")) | ||
| 107 | break :blk Arg { .arg = tmp[1..], .kind = Arg.Kind.Short }; | ||
| 108 | |||
| 109 | break :blk Arg { .arg = tmp, .kind = Arg.Kind.None }; | ||
| 110 | }; | ||
| 111 | const arg = pair.arg; | ||
| 112 | const kind = pair.kind; | ||
| 113 | |||
| 114 | comptime var required_index : usize = 0; | ||
| 115 | inline_loop: inline for (options) |option, op_i| { | ||
| 116 | |||
| 117 | switch (kind) { | ||
| 118 | Arg.Kind.None => { | ||
| 119 | if (option.short != null) continue :inline_loop; | ||
| 120 | if (option.long != null) continue :inline_loop; | ||
| 121 | |||
| 122 | try option.parser(&result, arg); | ||
| 123 | |||
| 124 | switch (option.kind) { | ||
| 125 | OptionT.Kind.Required => { | ||
| 126 | required = bits.set(u128, required, u7(required_index), false); | ||
| 127 | required_index += 1; | ||
| 128 | }, | ||
| 129 | OptionT.Kind.IgnoresRequired => { | ||
| 130 | required = 0; | ||
| 131 | required_index += 1; | ||
| 132 | }, | ||
| 133 | else => {} | ||
| 134 | } | ||
| 135 | |||
| 136 | continue :loop; | ||
| 137 | }, | ||
| 138 | Arg.Kind.Short => { | ||
| 139 | const short = option.short ?? continue :inline_loop; | ||
| 140 | if (arg.len != 1 or arg[0] != short) continue :inline_loop; | ||
| 141 | }, | ||
| 142 | Arg.Kind.Long => { | ||
| 143 | const long = option.long ?? continue :inline_loop; | ||
| 144 | if (!mem.eql(u8, long, arg)) continue :inline_loop; | ||
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | if (option.takes_value) arg_i += 1; | ||
| 149 | if (args.len <= arg_i) return error.MissingValueToArgument; | ||
| 150 | try option.parser(&result, args[arg_i]); | ||
| 151 | |||
| 152 | switch (option.kind) { | ||
| 153 | OptionT.Kind.Required => { | ||
| 154 | required = bits.set(u128, required, u7(required_index), false); | ||
| 155 | required_index += 1; | ||
| 156 | }, | ||
| 157 | OptionT.Kind.IgnoresRequired => { | ||
| 158 | required = 0; | ||
| 159 | required_index += 1; | ||
| 160 | }, | ||
| 161 | else => {} | ||
| 162 | } | ||
| 163 | |||
| 164 | continue :loop; | ||
| 165 | } else { | ||
| 166 | return error.InvalidArgument; | ||
| 167 | } | ||
| 168 | } | ||
| 169 | |||
| 170 | if (required != 0) { | ||
| 171 | return error.RequiredArgumentWasntHandled; | ||
| 172 | } | ||
| 173 | |||
| 174 | return result; | ||
| 175 | } | ||
| 176 | |||
| 177 | // TODO: | ||
| 178 | // * Usage | ||
| 179 | // * Description | ||
| 180 | pub fn help(out_stream: var) !void { | ||
| 181 | const equal_value : []const u8 = "=OPTION"; | ||
| 182 | const longest_long = comptime blk: { | ||
| 183 | var res = usize(0); | ||
| 184 | for (options) |option| { | ||
| 185 | const long = option.long ?? continue; | ||
| 186 | var len = long.len; | ||
| 187 | |||
| 188 | if (option.takes_value) | ||
| 189 | len += equal_value.len; | ||
| 190 | |||
| 191 | if (res < len) | ||
| 192 | res = len; | ||
| 193 | } | ||
| 194 | |||
| 195 | break :blk res; | ||
| 196 | }; | ||
| 197 | |||
| 198 | inline for (options) |option| { | ||
| 199 | if (option.short == null and option.long == null) continue; | ||
| 200 | |||
| 201 | try out_stream.print(" "); | ||
| 202 | if (option.short) |short| { | ||
| 203 | try out_stream.print("-{c}", short); | ||
| 204 | } else { | ||
| 205 | try out_stream.print(" "); | ||
| 206 | } | ||
| 207 | |||
| 208 | if (option.short != null and option.long != null) { | ||
| 209 | try out_stream.print(", "); | ||
| 210 | } else { | ||
| 211 | try out_stream.print(" "); | ||
| 212 | } | ||
| 213 | |||
| 214 | // We need to ident by: | ||
| 215 | // "--<longest_long> ".len | ||
| 216 | const missing_spaces = comptime blk: { | ||
| 217 | var res = longest_long + 3; | ||
| 218 | if (option.long) |long| { | ||
| 219 | res -= 2 + long.len; | ||
| 220 | |||
| 221 | if (option.takes_value) { | ||
| 222 | res -= equal_value.len; | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | break :blk res; | ||
| 227 | }; | ||
| 228 | |||
| 229 | if (option.long) |long| { | ||
| 230 | try out_stream.print("--{}", long); | ||
| 231 | |||
| 232 | if (option.takes_value) { | ||
| 233 | try out_stream.print("{}", equal_value); | ||
| 234 | } | ||
| 235 | } | ||
| 236 | |||
| 237 | try out_stream.print(" " ** missing_spaces); | ||
| 238 | try out_stream.print("{}\n", option.help_message); | ||
| 239 | } | ||
| 240 | } | ||
| 241 | }; | ||
| 242 | } | ||
| 243 | |||
| 244 | test "clap.parse.Example" { | ||
| 245 | const Color = struct { | ||
| 246 | const Self = this; | ||
| 247 | |||
| 248 | r: u8, g: u8, b: u8, | ||
| 249 | |||
| 250 | fn rFromStr(color: &Self, str: []const u8) !void { | ||
| 251 | color.r = try fmt.parseInt(u8, str, 10); | ||
| 252 | } | ||
| 253 | |||
| 254 | fn gFromStr(color: &Self, str: []const u8) !void { | ||
| 255 | color.g = try fmt.parseInt(u8, str, 10); | ||
| 256 | } | ||
| 257 | |||
| 258 | fn bFromStr(color: &Self, str: []const u8) !void { | ||
| 259 | color.b = try fmt.parseInt(u8, str, 10); | ||
| 260 | } | ||
| 261 | }; | ||
| 262 | |||
| 263 | const COption = Option(Color, @typeOf(Color.rFromStr).ReturnType.ErrorSet); | ||
| 264 | const options = comptime []COption { | ||
| 265 | COption.init(Color.rFromStr) | ||
| 266 | .setHelp("The amount of red in our color") | ||
| 267 | .setShort('r') | ||
| 268 | .setLong("red") | ||
| 269 | .takesValue(true) | ||
| 270 | .setKind(COption.Kind.Required), | ||
| 271 | COption.init(Color.gFromStr) | ||
| 272 | .setHelp("The amount of green in our color") | ||
| 273 | .setShort('g') | ||
| 274 | .setLong("green") | ||
| 275 | .takesValue(true), | ||
| 276 | COption.init(Color.bFromStr) | ||
| 277 | .setHelp("The amount of blue in our color") | ||
| 278 | .setShort('b') | ||
| 279 | .setLong("blue") | ||
| 280 | .takesValue(true), | ||
| 281 | }; | ||
| 282 | |||
| 283 | const Case = struct { args: []const []const u8, res: Color, err: ?error }; | ||
| 284 | const cases = []Case { | ||
| 285 | Case { | ||
| 286 | .args = [][]const u8 { "-r", "100", "-g", "100", "-b", "100", }, | ||
| 287 | .res = Color { .r = 100, .g = 100, .b = 100 }, | ||
| 288 | .err = null, | ||
| 289 | }, | ||
| 290 | Case { | ||
| 291 | .args = [][]const u8 { "--red", "100", "-g", "100", "--blue", "50", }, | ||
| 292 | .res = Color { .r = 100, .g = 100, .b = 50 }, | ||
| 293 | .err = null, | ||
| 294 | }, | ||
| 295 | Case { | ||
| 296 | .args = [][]const u8 { "-g", "200", "--blue", "100", "--red", "100", }, | ||
| 297 | .res = Color { .r = 100, .g = 200, .b = 100 }, | ||
| 298 | .err = null, | ||
| 299 | }, | ||
| 300 | Case { | ||
| 301 | .args = [][]const u8 { "-r", "200", "-r", "255" }, | ||
| 302 | .res = Color { .r = 255, .g = 0, .b = 0 }, | ||
| 303 | .err = null, | ||
| 304 | }, | ||
| 305 | Case { | ||
| 306 | .args = [][]const u8 { "-g", "200", "-b", "255" }, | ||
| 307 | .res = Color { .r = 0, .g = 0, .b = 0 }, | ||
| 308 | .err = error.RequiredArgumentWasntHandled, | ||
| 309 | }, | ||
| 310 | Case { | ||
| 311 | .args = [][]const u8 { "-p" }, | ||
| 312 | .res = Color { .r = 0, .g = 0, .b = 0 }, | ||
| 313 | .err = error.InvalidArgument, | ||
| 314 | }, | ||
| 315 | Case { | ||
| 316 | .args = [][]const u8 { "-g" }, | ||
| 317 | .res = Color { .r = 0, .g = 0, .b = 0 }, | ||
| 318 | .err = error.MissingValueToArgument, | ||
| 319 | }, | ||
| 320 | }; | ||
| 321 | |||
| 322 | const Clap = Parser(Color, @typeOf(Color.rFromStr).ReturnType.ErrorSet, Color { .r = 0, .g = 0, .b = 0 }, options); | ||
| 323 | for (cases) |case, i| { | ||
| 324 | if (Clap.parse(case.args)) |res| { | ||
| 325 | assert(res.r == case.res.r); | ||
| 326 | assert(res.g == case.res.g); | ||
| 327 | assert(res.b == case.res.b); | ||
| 328 | } else |err| { | ||
| 329 | assert(err == (case.err ?? unreachable)); | ||
| 330 | } | ||
| 331 | } | ||
| 332 | } | ||