Explorar o código

Add `core:flags`

Based on the Feoramund's original package
gingerBill hai 1 ano
pai
achega
f4dd48aa5d

+ 28 - 0
core/flags/LICENSE

@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2024, Feoramund, Ginger Bill
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 124 - 0
core/flags/README.md

@@ -0,0 +1,124 @@
+# `core:flags`
+
+`core:flags` is a complete command-line argument parser for the Odin programming
+language.
+
+It works by using Odin's run-time type information to determine where and how
+to store data on a struct provided by the user. Type conversion is handled
+automatically and errors are reported with useful messages.
+
+## Struct Tags
+
+Users of the `encoding/json` package may be familiar with using tags to
+annotate struct metadata. The same technique is used here to annotate where
+arguments should go and which are required.
+
+Under the `flags` tag:
+
+ - `name=S`, alias a struct field to `S`
+ - `pos=N`, place positional argument `N` into this field
+ - `hidden`, hide this field from the usage documentation
+ - `required`, cause verification to fail if this argument is not set
+
+There is also the `usage` tag, which is a plain string to be printed alongside
+the flag in the usage output.
+
+## Syntax
+
+Arguments are treated differently on how they're formatted. The format is
+similar to the Odin binary's way of handling compiler flags.
+
+```
+type                  handling
+------------          ------------------------
+<positional>          depends on struct layout
+-<flag>               set a bool to true
+-<flag:option>        set flag to option
+-<flag=option>        set flag to option, alternative syntax
+-<map>:<key>=<value>  set map[key] to value
+```
+
+## Complete Example
+
+```odin
+package main
+
+import "core:fmt"
+import "core:mem"
+import "core:os"
+import "core:path/filepath"
+
+import "core:flags"
+
+main :: proc() {
+	Options :: struct {
+		file: string `flags:"pos=0,required" usage:"input file"`,
+		out: string `flags:"pos=1" usage:"output file"`,
+		retry_count: uint `flags:"name=retries" usage:"times to retry process"`,
+		debug: bool `flags:"hidden" usage:"print debug info"`,
+		collection: map[string]string `usage:"path aliases"`,
+	}
+
+	opt: Options
+	program: string
+	args: []string
+
+	switch len(os.args) {
+	case 0:
+		flags.print_usage(&opt)
+		os.exit(0)
+	case:
+		program = filepath.base(os.args[0])
+		args = os.args[1:]
+	}
+
+	err := flags.parse(&opt, args)
+
+	switch subtype in err {
+	case mem.Allocator_Error:
+		fmt.println("allocation error:", subtype)
+		os.exit(1)
+	case flags.Parse_Error:
+		fmt.println(subtype.message)
+		os.exit(1)
+	case flags.Validation_Error:
+		fmt.println(subtype.message)
+		os.exit(1)
+	case flags.Help_Request:
+		flags.print_usage(&opt, program)
+		os.exit(0)
+	}
+
+	fmt.printf("%#v\n", opt)
+}
+```
+
+```
+$ ./odin-flags
+required argument `file` was not set
+
+$ ./odin-flags -help
+
+Usage:
+	odin-flags file [out] [-collection] [-retries]
+Flags:
+	-file:<string>                   input file
+	-out:<string>                    output file
+	-collection:<string>=<string>    path aliases
+	-retries:<uint>                  times to retry process
+
+$ ./odin-flags -retries:-3
+unable to set `retries` of type uint to `-3`
+
+$ ./odin-flags data -retries:3 -collection:core=./core -collection:runtime=./runtime
+Options{
+	file = "data",
+	out = "",
+	retry_count = 3,
+	debug = false,
+	collection = map[
+		core = "./core",
+		runtime = "./runtime",
+	],
+}
+```

+ 15 - 0
core/flags/constants.odin

@@ -0,0 +1,15 @@
+package flags
+
+TAG_FLAGS         :: "flags"
+SUBTAG_NAME       :: "name"
+SUBTAG_POS        :: "pos"
+SUBTAG_REQUIRED   :: "required"
+SUBTAG_HIDDEN     :: "hidden"
+
+TAG_USAGE         :: "usage"
+
+MINIMUM_SPACING   :: 4
+UNDOCUMENTED_FLAG :: "<This flag has not been documented yet.>"
+
+HARD_CODED_HELP_FLAG       :: "help"
+HARD_CODED_HELP_FLAG_SHORT :: "h"

+ 157 - 0
core/flags/conversion.odin

@@ -0,0 +1,157 @@
+package flags
+
+import "base:intrinsics"
+import "base:runtime"
+import "core:fmt"
+import "core:mem"
+import "core:reflect"
+
+_, _, _, _, _ :: intrinsics, runtime, fmt, mem, reflect
+
+// Add a positional argument to a data struct, checking for specified
+// positionals first before adding it to a fallback field.
+add_positional :: proc(data: ^$T, index: int, arg: string) -> Error {
+	field, has_pos_assigned := get_field_by_pos(data, index)
+
+	if !has_pos_assigned {
+		when !intrinsics.type_has_field(T, SUBTAG_POS) {
+			return Parse_Error {
+				.Extra_Pos,
+				fmt.tprintf("got extra positional argument `%s` with nowhere to store it", arg),
+			}
+		}
+
+		// Fall back to adding it to a dynamic array named `pos`. 
+		field = reflect.struct_field_by_name(T, SUBTAG_POS)
+		assert(field.type != nil, "this should never happen")
+	}
+
+	ptr := cast(rawptr)(uintptr(data) + field.offset)
+	if !parse_and_set_pointer_by_type(ptr, arg, field.type) {
+		return Parse_Error {
+			.Bad_Type,
+			fmt.tprintf("unable to set positional %i (%s) of type %v to `%s`", index, field.name, field.type, arg),
+		}
+	}
+
+	return nil
+}
+
+// Set a `-flag` argument.
+set_flag :: proc(data: ^$T, name: string) -> Error {
+	// We make a special case for help requests.
+	switch name {
+	case HARD_CODED_HELP_FLAG:
+		fallthrough
+	case HARD_CODED_HELP_FLAG_SHORT:
+		return Help_Request{}
+	}
+
+	field := get_field_by_name(data, name) or_return
+
+	#partial switch t in field.type.variant {
+	case runtime.Type_Info_Boolean:
+		ptr := cast(^bool)(uintptr(data) + field.offset)
+		ptr^ = true
+	case:
+		return Parse_Error {
+			.Bad_Type,
+			fmt.tprintf("unable to set `%s` of type %v to true", name, field.type),
+		}
+	}
+
+	return nil
+}
+
+// Set a `-flag:option` argument.
+set_option :: proc(data: ^$T, name, option: string) -> Error {
+	field := get_field_by_name(data, name) or_return
+
+	// Guard against incorrect syntax.
+	#partial switch t in field.type.variant {
+	case runtime.Type_Info_Map:
+		return Parse_Error {
+			.Missing_Value,
+			fmt.tprintf("unable to set `%s` of type %v to `%s`, are you missing an `=`?", name, field.type, option),
+		}
+	}
+
+	ptr := rawptr(uintptr(data) + field.offset)
+	if !parse_and_set_pointer_by_type(ptr, option, field.type) {
+		return Parse_Error {
+			.Bad_Type,
+			fmt.tprintf("unable to set `%s` of type %v to `%s`", name, field.type, option),
+		}
+	}
+
+	return nil
+}
+
+// Set a `-map:key=value` argument.
+set_key_value :: proc(data: ^$T, name, key, value: string) -> Error {
+	field := get_field_by_name(data, name) or_return
+
+	#partial switch t in field.type.variant {
+	case runtime.Type_Info_Map:
+		if !reflect.is_string(t.key) {
+			return Parse_Error {
+				.Bad_Type,
+				fmt.tprintf("`%s` must be a map[string]", name),
+			}
+		}
+
+		key := key
+		key_ptr := rawptr(&key)
+		key_cstr: cstring
+		if reflect.is_cstring(t.key) {
+			key_cstr = cstring(raw_data(key))
+			key_ptr = &key_cstr
+		}
+
+		raw_map := (^runtime.Raw_Map)(uintptr(data) + field.offset)
+
+		hash := t.map_info.key_hasher(key_ptr, runtime.map_seed(raw_map^))
+
+		backing_alloc := false
+		elem_backing: []byte
+		value_ptr: rawptr
+
+		if raw_map.allocator.procedure == nil {
+			raw_map.allocator = context.allocator
+		} else {
+			value_ptr = runtime.__dynamic_map_get(raw_map,
+				t.map_info,
+				hash,
+				key_ptr,
+			)
+		}
+
+		if value_ptr == nil {
+			elem_backing = mem.alloc_bytes(t.value.size, t.value.align) or_return
+			backing_alloc = true
+			value_ptr = raw_data(elem_backing)
+		}
+
+		if !parse_and_set_pointer_by_type(value_ptr, value, t.value) {
+			break
+		}
+
+		if backing_alloc {
+			runtime.__dynamic_map_set(raw_map,
+				t.map_info,
+				hash,
+				key_ptr,
+				value_ptr,
+			)
+
+			delete(elem_backing)
+		}
+
+		return nil
+	}
+
+	return Parse_Error {
+		.Bad_Type,
+		fmt.tprintf("unable to set `%s` of type %v with key=value `%s` = `%s`", name, field.type, key, value),
+	}
+}

+ 92 - 0
core/flags/doc.odin

@@ -0,0 +1,92 @@
+/*
+package flags implements a command-line argument parser.
+
+It works by using Odin's run-time type information to determine where and how
+to store data on a struct provided by the user. Type conversion is handled
+automatically and errors are reported with useful messages.
+
+
+Command-Line Syntax:
+
+Arguments are treated differently on how they're formatted. The format is
+similar to the Odin binary's way of handling compiler flags.
+
+```
+type                  handling
+------------          ------------------------
+<positional>          depends on struct layout
+-<flag>               set a bool true
+-<flag:option>        set flag to option
+-<flag=option>        set flag to option, alternative syntax
+-<map>:<key>=<value>  set map[key] to value
+```
+
+
+Struct Tags:
+
+Users of the `encoding/json` package may be familiar with using tags to
+annotate struct metadata. The same technique is used here to annotate where
+arguments should go and which are required.
+
+Under the `args` tag:
+
+ - `name=S`, alias a struct field to `S`
+ - `pos=N`, place positional argument `N` into this field
+ - `hidden`, hide this field from the usage documentation
+ - `required`, cause verification to fail if this argument is not set
+
+There is also the `usage` tag, which is a plain string to be printed alongside
+the flag in the usage output.
+
+
+Supported Field Datatypes:
+
+- all `bool`s
+- all `int`s
+- all `float`s
+- `string`, `cstring`
+- `rune`
+- `dynamic` arrays with element types of the above
+- `map[string]`s with value types of the above
+
+
+Validation:
+
+The parser will ensure `required` arguments are set. This is on by default.
+
+
+Strict:
+
+The parser will return on the first error and stop parsing. This is on by
+default. Otherwise, all arguments that can be parsed, will be, and only the
+last error is returned.
+
+
+Help:
+
+By default, `-h` and `-help` are reserved flags which raise their own error
+type when set, allowing the program to handle the request differently from
+other errors.
+
+
+Example:
+
+```odin
+	Options :: struct {
+		file: string `args:"pos=0,required" usage:"input file"`,
+		out: string `args:"pos=1" usage:"output file"`,
+		retry_count: uint `args:"name=retries" usage:"times to retry process"`,
+		debug: bool `args:"hidden" usage:"print debug info"`,
+		collection: map[string]string `usage:"path aliases"`,
+	}
+
+	opt: Options
+	flags.parse(&opt, {
+		"main.odin",
+		"-retries:3",
+		"-collection:core=./core",
+		"-debug",
+	}, validate_args = true, strict = true)
+```
+*/
+package flags

+ 29 - 0
core/flags/errors.odin

@@ -0,0 +1,29 @@
+package flags
+
+import "base:runtime"
+
+Parse_Error_Type :: enum {
+	None,
+	Extra_Pos,
+	Bad_Type,
+	Missing_Field,
+	Missing_Value,
+}
+
+Parse_Error :: struct {
+	type: Parse_Error_Type,
+	message: string,
+}
+
+Validation_Error :: struct {
+	message: string,
+}
+
+Help_Request :: distinct bool
+
+Error :: union {
+	runtime.Allocator_Error,
+	Parse_Error,
+	Validation_Error,
+	Help_Request,
+}

+ 86 - 0
core/flags/parsing.odin

@@ -0,0 +1,86 @@
+package flags
+
+import "core:strings"
+_ :: strings
+
+@(private)
+parse_one_arg :: proc(data: ^$T, arg: string, pos: ^int, set_args: ^[dynamic]string) -> (err: Error) {
+	arg := arg
+
+	if strings.has_prefix(arg, "-") {
+		arg = arg[1:]
+
+		if colon := strings.index_byte(arg, ':'); colon != -1 {
+			flag := arg[:colon]
+			arg = arg[1 + colon:]
+
+			if equals := strings.index_byte(arg, '='); equals != -1 {
+				// -map:key=value
+				key := arg[:equals]
+				value := arg[1 + equals:]
+				set_key_value(data, flag, key, value) or_return
+				append(set_args, flag)
+			} else {
+				// -flag:option
+				set_option(data, flag, arg) or_return
+				append(set_args, flag)
+			}
+
+		} else if equals := strings.index_byte(arg, '='); equals != -1 {
+			// -flag=option, alternative syntax
+			flag := arg[:equals]
+			arg = arg[1 + equals:]
+
+			set_option(data, flag, arg) or_return
+			append(set_args, flag)
+		} else {
+			// -flag
+			set_flag(data, arg) or_return
+			append(set_args, arg)
+		}
+
+	} else {
+		// positional
+		err = add_positional(data, pos^, arg)
+		pos^ += 1
+	}
+
+	return
+}
+
+// Parse a slice of command-line arguments into an annotated struct.
+//
+// If `validate_args` is set, an error will be returned if all required
+// arguments are not set. This step is only completed if there were no errors
+// from parsing.
+//
+// If `strict` is set, an error will cause parsing to stop and the procedure
+// will return with the message. Otherwise, parsing will continue and only the
+// last error will be returned.
+parse :: proc(data: ^$T, args: []string, validate_args: bool = true, strict: bool = true) -> (err: Error) {
+	// For checking required arguments.
+	set_args: [dynamic]string
+	defer delete(set_args)
+
+	// Positional argument tracker.
+	pos := 0
+
+	if strict {
+		for arg in args {
+			parse_one_arg(data, arg, &pos, &set_args) or_return
+		}
+	} else {
+		for arg in args {
+			this_error := parse_one_arg(data, arg, &pos, &set_args)
+			if this_error != nil {
+				err = this_error
+			}
+		}
+	}
+
+	if err == nil && validate_args {
+		return validate(data, pos, set_args[:])
+	}
+
+	return err
+}

+ 139 - 0
core/flags/usage.odin

@@ -0,0 +1,139 @@
+package flags
+
+import "base:runtime"
+import "core:fmt"
+import "core:io"
+import "core:os"
+import "core:reflect"
+import "core:slice"
+import "core:strconv"
+import "core:strings"
+
+_, _, _, _, _, _, _, _ :: runtime, fmt, io, os, reflect, slice, strconv, strings
+
+// Write out the documentation for the command-line arguments.
+write_usage :: proc(out: io.Writer, data: ^$T, program: string = "") {
+	Flag :: struct {
+		name:           string,
+		usage:          string,
+		name_with_type: string,
+		pos:            int,
+		is_positional:  bool,
+		is_required:    bool,
+		is_boolean:     bool,
+		is_hidden:      bool,
+	}
+
+	sort_flags :: proc(a, b: Flag) -> slice.Ordering {
+		if a.is_positional && b.is_positional {
+			return slice.cmp(a.pos, b.pos)
+		}
+
+		if a.is_required && !b.is_required {
+			return .Less
+		} else if !a.is_required && b.is_required {
+			return .Greater
+		}
+
+		if a.is_positional && !b.is_positional {
+			return .Less
+		} else if b.is_positional && !a.is_positional {
+			return .Greater
+		}
+
+		return slice.cmp(a.name, b.name)
+	}
+
+	flags: [dynamic]Flag
+	defer delete(flags)
+
+	longest_flag_length: int
+
+	for field in reflect.struct_fields_zipped(T) {
+		flag: Flag
+
+		flag.name = get_field_name(field)
+		#partial switch t in field.type.variant {
+		case runtime.Type_Info_Map:
+			flag.name_with_type = fmt.tprintf("%s:<%v>=<%v>", flag.name, t.key.id, t.value.id)
+		case runtime.Type_Info_Dynamic_Array:
+			flag.name_with_type = fmt.tprintf("%s:<%v, ...>", flag.name, t.elem.id)
+		case:
+			flag.name_with_type = fmt.tprintf("%s:<%v>", flag.name, field.type.id)
+		}
+
+		if usage, ok := reflect.struct_tag_lookup(field.tag, TAG_USAGE); ok {
+			flag.usage = usage
+		} else {
+			flag.usage = UNDOCUMENTED_FLAG
+		}
+
+		if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
+			if pos_str, is_pos := get_struct_subtag(args_tag, SUBTAG_POS); is_pos {
+				flag.is_positional = true
+				if pos, ok := strconv.parse_int(pos_str); ok && pos >= 0 {
+					flag.pos = pos
+				} else {
+					fmt.panicf("%v has incorrect pos subtag specifier `%s`", typeid_of(T), pos_str)
+				}
+			}
+			if _, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
+				flag.is_required = true
+			}
+			if reflect.type_kind(field.type.id) == .Boolean {
+				flag.is_boolean = true
+			}
+			if _, is_hidden := get_struct_subtag(args_tag, SUBTAG_HIDDEN); is_hidden {
+				flag.is_hidden = true
+			}
+		}
+
+		if !flag.is_hidden {
+			longest_flag_length = max(longest_flag_length, len(flag.name_with_type))
+		}
+
+		append(&flags, flag)
+	}
+
+	slice.sort_by_cmp(flags[:], sort_flags)
+
+	if len(program) > 0 {
+		fmt.wprintf(out, "Usage:\n\t%s", program)
+
+		for flag in flags {
+			if flag.is_hidden {
+				continue
+			}
+
+			io.write_byte(out, ' ')
+
+			if flag.name == SUBTAG_POS {
+				io.write_string(out, "...")
+				continue
+			}
+
+			if !flag.is_required   { io.write_byte(out, '[') }
+			if !flag.is_positional { io.write_byte(out, '-') }
+			io.write_string(out, flag.name)
+			if !flag.is_required   { io.write_byte(out, ']') }
+		}
+		io.write_byte(out, '\n')
+	}
+
+	fmt.wprintln(out, "Flags:")
+	for flag in flags {
+		if flag.is_hidden {
+			continue
+		}
+
+		spacing := strings.repeat(" ",
+			(MINIMUM_SPACING + longest_flag_length) - len(flag.name_with_type),
+			context.temp_allocator)
+		fmt.wprintf(out, "\t-%s%s%s\n", flag.name_with_type, spacing, flag.usage)
+	}
+}
+
+// Print out the documentation for the command-line arguments.
+print_usage :: proc(data: ^$T, program: string = "") {
+	write_usage(os.stream_from_handle(os.stdout), data, program)
+}

+ 189 - 0
core/flags/util.odin

@@ -0,0 +1,189 @@
+package flags
+
+import "base:runtime"
+import "core:fmt"
+import "core:mem"
+import "core:reflect"
+import "core:strconv"
+import "core:strings"
+import "core:unicode/utf8"
+
+_, _, _, _, _, _, _ :: runtime, fmt, mem, reflect, strconv, strings, utf8
+
+@(private)
+parse_and_set_pointer_by_type :: proc(ptr: rawptr, value: string, ti: ^runtime.Type_Info) -> bool {
+	set_bool :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
+		(^T)(ptr)^ = (T)(strconv.parse_bool(str) or_return)
+		return true
+	}
+
+	set_i128 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
+		value := strconv.parse_i128(str) or_return
+		if value > cast(i128)max(T) || value < cast(i128)min(T) {
+			return false
+		}
+		(^T)(ptr)^ = (T)(value)
+		return true
+	}
+
+	set_u128 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
+		value := strconv.parse_u128(str) or_return
+		if value > cast(u128)max(T) {
+			return false
+		}
+		(^T)(ptr)^ = (T)(value)
+		return true
+	}
+
+	set_f64 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
+		(^T)(ptr)^ = (T)(strconv.parse_f64(str) or_return)
+		return true
+	}
+
+	a := any{ptr, ti.id}
+
+	#partial switch t in ti.variant {
+	case runtime.Type_Info_Dynamic_Array:
+		ptr := (^runtime.Raw_Dynamic_Array)(ptr)
+
+		// Try to convert the value first.
+		elem_backing, mem_err := mem.alloc_bytes(t.elem.size, t.elem.align)
+		if mem_err != nil {
+			return false
+		}
+		defer delete(elem_backing)
+		parse_and_set_pointer_by_type(raw_data(elem_backing), value, t.elem) or_return
+
+		runtime.__dynamic_array_resize(ptr, t.elem.size, t.elem.align, ptr.len + 1) or_return
+		subptr := cast(rawptr)(uintptr(ptr.data) + uintptr((ptr.len - 1) * t.elem.size))
+		mem.copy(subptr, raw_data(elem_backing), len(elem_backing))
+
+	case runtime.Type_Info_Boolean:
+		switch b in a {
+		case bool: set_bool(ptr, bool, value) or_return
+		case b8:   set_bool(ptr, b8,   value) or_return
+		case b16:  set_bool(ptr, b16,  value) or_return
+		case b32:  set_bool(ptr, b32,  value) or_return
+		case b64:  set_bool(ptr, b64,  value) or_return
+		}
+
+	case runtime.Type_Info_Rune:
+		r := utf8.rune_at_pos(value, 0)
+		if r == utf8.RUNE_ERROR { return false }
+		(^rune)(ptr)^ = r
+
+	case runtime.Type_Info_String:
+		switch s in a {
+		case string:  (^string)(ptr)^ = value
+		case cstring: (^cstring)(ptr)^ = strings.clone_to_cstring(value)
+		}
+	case runtime.Type_Info_Integer:
+		switch i in a {
+		case int:     set_i128(ptr, int,     value) or_return
+		case i8:      set_i128(ptr, i8,      value) or_return
+		case i16:     set_i128(ptr, i16,     value) or_return
+		case i32:     set_i128(ptr, i32,     value) or_return
+		case i64:     set_i128(ptr, i64,     value) or_return
+		case i128:    set_i128(ptr, i128,    value) or_return
+		case i16le:   set_i128(ptr, i16le,   value) or_return
+		case i32le:   set_i128(ptr, i32le,   value) or_return
+		case i64le:   set_i128(ptr, i64le,   value) or_return
+		case i128le:  set_i128(ptr, i128le,  value) or_return
+		case i16be:   set_i128(ptr, i16be,   value) or_return
+		case i32be:   set_i128(ptr, i32be,   value) or_return
+		case i64be:   set_i128(ptr, i64be,   value) or_return
+		case i128be:  set_i128(ptr, i128be,  value) or_return
+
+		case uint:    set_u128(ptr, uint,    value) or_return
+		case uintptr: set_u128(ptr, uintptr, value) or_return
+		case u8:      set_u128(ptr, u8,      value) or_return
+		case u16:     set_u128(ptr, u16,     value) or_return
+		case u32:     set_u128(ptr, u32,     value) or_return
+		case u64:     set_u128(ptr, u64,     value) or_return
+		case u128:    set_u128(ptr, u128,    value) or_return
+		case u16le:   set_u128(ptr, u16le,   value) or_return
+		case u32le:   set_u128(ptr, u32le,   value) or_return
+		case u64le:   set_u128(ptr, u64le,   value) or_return
+		case u128le:  set_u128(ptr, u128le,  value) or_return
+		case u16be:   set_u128(ptr, u16be,   value) or_return
+		case u32be:   set_u128(ptr, u32be,   value) or_return
+		case u64be:   set_u128(ptr, u64be,   value) or_return
+		case u128be:  set_u128(ptr, u128be,  value) or_return
+		}
+	case runtime.Type_Info_Float:
+		switch f in a {
+		case f16:   set_f64(ptr, f16,   value) or_return
+		case f32:   set_f64(ptr, f32,   value) or_return
+		case f64:   set_f64(ptr, f64,   value) or_return
+
+		case f16le: set_f64(ptr, f16le, value) or_return
+		case f32le: set_f64(ptr, f32le, value) or_return
+		case f64le: set_f64(ptr, f64le, value) or_return
+
+		case f16be: set_f64(ptr, f16be, value) or_return
+		case f32be: set_f64(ptr, f32be, value) or_return
+		case f64be: set_f64(ptr, f64be, value) or_return
+		}
+	case:
+		return false
+	}
+
+	return true
+}
+
+@(private)
+get_struct_subtag :: proc(tag, id: string) -> (value: string, ok: bool) {
+	tag := tag
+
+	for subtag in strings.split_iterator(&tag, ",") {
+		if equals := strings.index_byte(subtag, '='); equals != -1 && id == subtag[:equals] {
+			return subtag[1 + equals:], true
+		} else if id == subtag {
+			return "", true
+		}
+	}
+
+	return "", false
+}
+
+@(private)
+get_field_name :: proc(field: reflect.Struct_Field) -> string {
+	if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_FLAGS); ok {
+		if name_subtag, name_ok := get_struct_subtag(args_tag, SUBTAG_NAME); name_ok {
+			return name_subtag
+		}
+	}
+
+	return field.name
+}
+
+// Get a struct field by its field name or "name" subtag.
+// NOTE: `Error` uses the `context.temp_allocator` to give context about the error message
+get_field_by_name :: proc(data: ^$T, name: string) -> (field: reflect.Struct_Field, err: Error) {
+	for field in reflect.struct_fields_zipped(T) {
+		if get_field_name(field) == name {
+			return field, nil
+		}
+	}
+
+	return {}, Parse_Error {
+		.Missing_Field,
+		fmt.tprintf("unable to find argument by name `%s`", name),
+	}
+}
+
+// Get a struct field by its "pos" subtag.
+get_field_by_pos :: proc(data: ^$T, index: int) -> (field: reflect.Struct_Field, ok: bool) {
+	fields := reflect.struct_fields_zipped(T)
+
+	for field in fields {
+		args_tag   := reflect.struct_tag_lookup(field.tag, TAG_FLAGS) or_continue
+		pos_subtag := get_struct_subtag(args_tag, SUBTAG_POS) or_continue
+		value      := strconv.parse_int(pos_subtag) or_continue
+		if value == index {
+			return field, true
+		}
+	}
+
+	return {}, false
+}

+ 45 - 0
core/flags/validation.odin

@@ -0,0 +1,45 @@
+package flags
+
+import "core:fmt"
+import "core:reflect"
+import "core:strconv"
+
+_ :: fmt
+_ :: reflect
+_ :: strconv
+
+// Validate that all the required arguments are set.
+validate :: proc(data: ^$T, max_pos: int, set_args: []string) -> Error {
+	fields := reflect.struct_fields_zipped(T)
+
+	check_fields: for field in fields {
+		tag := reflect.struct_tag_lookup(field.tag, TAG_ARGS) or_continue
+		if _, ok := get_struct_subtag(tag, SUBTAG_REQUIRED); ok {
+			was_set := false
+
+			// Check if it was set by name.
+			check_set_args: for set_arg in set_args {
+				if get_field_name(field) == set_arg {
+					was_set = true
+					break check_set_args
+				}
+			}
+
+			// Check if it was set by position.
+			if pos, has_pos := get_struct_subtag(tag, SUBTAG_POS); has_pos {
+				value, value_ok := strconv.parse_int(pos)
+				if value < max_pos {
+					was_set = true
+				}
+			}
+
+			if !was_set {
+				return Validation_Error {
+					fmt.tprintf("required argument `%s` was not set", field.name),
+				}
+			}
+		}
+	}
+
+	return nil
+}