Browse Source

Add package `core:flags`

Feoramund 1 year ago
parent
commit
edb685f04b

+ 28 - 0
core/flags/LICENSE

@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2024, Feoramund
+
+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.

+ 38 - 0
core/flags/constants.odin

@@ -0,0 +1,38 @@
+package flags
+
+import "core:time"
+
+// Set to true to compile with support for core named types disabled, as a
+// fallback in the event your platform does not support one of the types, or
+// you have no need for them and want a smaller binary.
+NO_CORE_NAMED_TYPES :: #config(ODIN_CORE_FLAGS_NO_CORE_NAMED_TYPES, false)
+
+// Override support for parsing `time` types.
+IMPORTING_TIME      :: #config(ODIN_CORE_FLAGS_USE_TIME, time.IS_SUPPORTED)
+
+// Override support for parsing `net` types.
+// TODO: Update this when the BSDs are supported.
+IMPORTING_NET       :: #config(ODIN_CORE_FLAGS_USE_NET, ODIN_OS == .Windows || ODIN_OS == .Linux || ODIN_OS == .Darwin)
+
+TAG_ARGS          :: "args"
+SUBTAG_NAME       :: "name"
+SUBTAG_POS        :: "pos"
+SUBTAG_REQUIRED   :: "required"
+SUBTAG_HIDDEN     :: "hidden"
+SUBTAG_VARIADIC   :: "variadic"
+SUBTAG_FILE       :: "file"
+SUBTAG_PERMS      :: "perms"
+SUBTAG_INDISTINCT :: "indistinct"
+
+TAG_USAGE         :: "usage"
+
+UNDOCUMENTED_FLAG :: "<This flag has not been documented yet.>"
+
+INTERNAL_VARIADIC_FLAG   :: "varg"
+
+RESERVED_HELP_FLAG       :: "help"
+RESERVED_HELP_FLAG_SHORT :: "h"
+
+// If there are more than this number of flags in total, only the required and
+// positional flags will be shown in the one-line usage summary.
+ONE_LINE_FLAG_CUTOFF_COUNT :: 16

+ 181 - 0
core/flags/doc.odin

@@ -0,0 +1,181 @@
+/*
+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 program. Type conversion is handled
+automatically and errors are reported with useful messages.
+
+
+Command-Line Syntax:
+
+Arguments are treated differently depending 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 `core: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, there are the following subtags:
+
+- `name=S`: set `S` as the flag's name.
+- `pos=N`: place positional argument `N` into this flag.
+- `hidden`: hide this flag from the usage documentation.
+- `required`: cause verification to fail if this argument is not set.
+- `variadic`: take all remaining arguments when set, UNIX-style only.
+- `file`: for `os.Handle` types, file open mode.
+- `perms`: for `os.Handle` types, file open permissions.
+- `indistinct`: allow the setting of distinct types by their base type.
+
+`required` may be given a range specifier in the following formats:
+```
+min
+<max
+min<max
+```
+
+`max` is not inclusive in this range, as noted by the less-than `<` sign, so if
+you want to require 3 and only 3 arguments in a dynamic array, you would
+specify `required=3<4`.
+
+
+`variadic` may be given a number (`variadic=N`) above 1 to limit how many extra
+arguments it consumes.
+
+
+`file` determines the file open mode for an `os.Handle`.
+It accepts a string of flags that can be mixed together:
+- r: read
+- w: write
+- c: create, create the file if it doesn't exist
+- a: append, add any new writes to the end of the file
+- t: truncate, erase the file on open
+
+
+`perms` determines the file open permissions for an `os.Handle`.
+
+The permissions are represented by three numbers in octal format. The first
+number is the owner, the second is the group, and the third is other. Read is
+represented by 4, write by 2, and execute by 1.
+
+These numbers are added together to get combined permissions. For example, 644
+represents read/write for the owner, read for the group, and read for other.
+
+Note that this may only have effect on UNIX-like platforms. By default, `perms`
+is set to 444 when only reading and 644 when writing.
+
+
+`indistinct` tells the parser that it's okay to treat distinct types as their
+underlying base type. Normally, the parser will hand those types off to the
+custom type setter (more about that later) if one is available, if it doesn't
+know how to handle the type.
+
+
+Usage Tag:
+
+There is also the `usage` tag, which is a plain string to be printed alongside
+the flag in the usage output. If `usage` contains a newline, it will be
+properly aligned when printed.
+
+All surrounding whitespace is trimmed when formatting with multiple lines.
+
+
+Supported Flag Data Types:
+
+- all booleans
+- all integers
+- all floats
+- all enums
+- all complex numbers
+- all quaternions
+- all bit_sets
+- `string` and `cstring`
+- `rune`
+- `os.Handle`
+- `time.Time`
+- `datetime.DateTime`
+- `net.Host_Or_Endpoint`,
+- additional custom types, see Custom Types below
+- `dynamic` arrays with element types of the above
+- `map[string]`s or `map[cstring]`s with value types of the above
+
+
+Validation:
+
+The parser will ensure `required` arguments are set, if no errors occurred
+during parsing. This is on by default.
+
+Additionally, you may call `register_flag_checker` to set your own argument
+validation procedure that will be called after the default checker.
+
+
+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.
+
+
+Error Messages:
+
+All error message strings are allocated using the context's `temp_allocator`,
+so if you need them to persist, make sure to clone the underlying `message`.
+
+
+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.
+
+
+Custom Types:
+
+You may specify your own type setter for program-specific structs and other
+named types. Call `register_type_setter` with an appropriate proc before
+calling any of the parsing procs.
+
+A compliant `Custom_Type_Setter` must return three values:
+- an error message if one occurred,
+- a boolean indicating if the proc handles the type, and
+- an `Allocator_Error` if any occurred.
+
+If the setter does not handle the type, simply return without setting any of
+the values.
+
+
+UNIX-style:
+
+This package also supports parsing arguments in a limited flavor of UNIX.
+Odin and UNIX style are mutually exclusive, and which one to be used is chosen
+at parse time.
+
+```
+--flag
+--flag=argument
+--flag argument
+--flag argument repeating-argument
+```
+
+`-flag` may also be substituted for `--flag`.
+
+Do note that map flags are not currently supported in this parsing style.
+
+
+Example:
+
+A complete example is given in the `example` subdirectory.
+*/
+package flags

+ 58 - 0
core/flags/errors.odin

@@ -0,0 +1,58 @@
+package flags
+
+import "base:runtime"
+import "core:net"
+import "core:os"
+
+Parse_Error_Reason :: enum {
+	None,
+	// An extra positional argument was given, and there is no `varg` field.
+	Extra_Positional,
+	// The underlying type does not support the string value it is being set to.
+	Bad_Value,
+	// No flag was given by the user.
+	No_Flag,
+	// No value was given by the user.
+	No_Value,
+	// The flag on the struct is missing.
+	Missing_Flag,
+	// The type itself isn't supported.
+	Unsupported_Type,
+}
+
+Unified_Parse_Error_Reason :: union #shared_nil {
+	Parse_Error_Reason,
+	runtime.Allocator_Error,
+	net.Parse_Endpoint_Error,
+}
+
+// Raised during parsing, naturally.
+Parse_Error :: struct {
+	reason: Unified_Parse_Error_Reason,
+	message: string,
+}
+
+// Raised during parsing.
+// Provides more granular information than what just a string could hold.
+Open_File_Error :: struct {
+	filename: string,
+	errno: os.Errno,
+	mode: int,
+	perms: int,
+}
+
+// Raised during parsing.
+Help_Request :: distinct bool
+
+
+// Raised after parsing, during validation.
+Validation_Error :: struct {
+	message: string,
+}
+
+Error :: union {
+	Parse_Error,
+	Open_File_Error,
+	Help_Request,
+	Validation_Error,
+}

+ 132 - 0
core/flags/example/example.odin

@@ -0,0 +1,132 @@
+package core_flags_example
+
+import "base:runtime"
+import "core:flags"
+import "core:fmt"
+import "core:net"
+import "core:os"
+import "core:time/datetime"
+
+
+Fixed_Point1_1 :: struct {
+	integer: u8,
+	fractional: u8,
+}
+
+Optimization_Level :: enum {
+	Slow,
+	Fast,
+	Warp_Speed,
+	Ludicrous_Speed,
+}
+
+// It's simple but powerful.
+my_custom_type_setter :: proc(
+	data: rawptr,
+	data_type: typeid,
+	unparsed_value: string,
+	args_tag: string,
+) -> (
+	error: string,
+	handled: bool,
+	alloc_error: runtime.Allocator_Error,
+) {
+	if data_type == Fixed_Point1_1 {
+		handled = true
+		ptr := cast(^Fixed_Point1_1)data
+
+		// precision := flags.get_subtag(args_tag, "precision")
+
+		if len(unparsed_value) == 3 {
+			ptr.integer = unparsed_value[0] - '0'
+			ptr.fractional = unparsed_value[2] - '0'
+		} else {
+			error = "Incorrect format. Must be in the form of `i.f`."
+		}
+
+		// Perform sanity checking here in the type parsing phase.
+		//
+		// The validation phase is flag-specific.
+		if !(0 <= ptr.integer && ptr.integer < 10) || !(0 <= ptr.fractional && ptr.fractional < 10) {
+			error = "Incorrect format. Must be between `0.0` and `9.9`."
+		}
+	}
+
+	return
+}
+
+my_custom_flag_checker :: proc(
+	model: rawptr,
+	name: string,
+	value: any,
+	args_tag: string,
+) -> (error: string) {
+	if name == "iterations" {
+		v := value.(int)
+		if !(1 <= v && v < 5) {
+			error = "Iterations only supports 1 ..< 5."
+		}
+	}
+
+	return
+}
+
+Distinct_Int :: distinct int
+
+main :: proc() {
+	Options :: struct {
+
+		file: os.Handle `args:"pos=0,required,file=r" usage:"Input file."`,
+		output: os.Handle `args:"pos=1,file=cw" usage:"Output file."`,
+
+		hub: net.Host_Or_Endpoint `usage:"Internet address to contact for updates."`,
+		schedule: datetime.DateTime `usage:"Launch tasks at this time."`,
+
+		opt: Optimization_Level `usage:"Optimization level."`,
+		todo: [dynamic]string `usage:"Todo items."`,
+
+		accuracy: Fixed_Point1_1 `args:"required" usage:"Lenience in FLOP calculations."`,
+		iterations: int `usage:"Run this many times."`,
+
+		// Note how the parser will transform this flag's name into `special-int`.
+		special_int: Distinct_Int `args:"indistinct" usage:"Able to set distinct types."`,
+
+		quat: quaternion256,
+
+		bits: bit_set[0..<8],
+
+		// Many different requirement styles:
+
+		// gadgets: [dynamic]string `args:"required=1" usage:"gadgets"`,
+		// widgets: [dynamic]string `args:"required=<3" usage:"widgets"`,
+		// foos: [dynamic]string `args:"required=2<4"`,
+		// bars: [dynamic]string `args:"required=3<4"`,
+		// bots: [dynamic]string `args:"required"`,
+
+		// (Maps) Only available in Odin style:
+
+		// assignments: map[string]u8 `args:"name=assign" usage:"Number of jobs per worker."`,
+
+		// (Variadic) Only available in UNIX style:
+
+		// bots: [dynamic]string `args:"variadic=2,required"`,
+
+		verbose: bool `usage:"Show verbose output."`,
+		debug: bool `args:"hidden" usage:"print debug info"`,
+
+		varg: [dynamic]string `usage:"Any extra arguments go here."`,
+	}
+
+	opt: Options
+	style : flags.Parsing_Style = .Odin
+
+	flags.register_type_setter(my_custom_type_setter)
+	flags.register_flag_checker(my_custom_flag_checker)
+	flags.parse_or_exit(&opt, os.args, style)
+
+	fmt.printfln("%#v", opt)
+
+	if opt.output != 0 {
+		os.write_string(opt.output, "Hellope!\n")
+	}
+}

+ 262 - 0
core/flags/internal_assignment.odin

@@ -0,0 +1,262 @@
+//+private
+package flags
+
+import "base:intrinsics"
+import "base:runtime"
+import "core:container/bit_array"
+import "core:fmt"
+import "core:mem"
+import "core:reflect"
+import "core:strconv"
+import "core:strings"
+
+// Push a positional argument onto a data struct, checking for specified
+// positionals first before adding it to a fallback field.
+@(optimization_mode="size")
+push_positional :: #force_no_inline proc (model: ^$T, parser: ^Parser, arg: string) -> (error: Error) {
+	if bit_array.get(&parser.filled_pos, parser.filled_pos.max_index) {
+		// The max index is set, which means we're out of space.
+		// Add one free bit by setting the index above to false.
+		bit_array.set(&parser.filled_pos, 1 + parser.filled_pos.max_index, false)
+	}
+
+	pos: int = ---
+	{
+		iter := bit_array.make_iterator(&parser.filled_pos)
+		ok: bool
+		pos, ok = bit_array.iterate_by_unset(&iter)
+
+		// This may be an allocator error.
+		assert(ok, "Unable to find a free spot in the positional bit_array.")
+	}
+
+	field, index, has_pos_assigned := get_field_by_pos(model, pos)
+
+	if !has_pos_assigned {
+		when intrinsics.type_has_field(T, INTERNAL_VARIADIC_FLAG) {
+			// Add it to the fallback array.
+			field = reflect.struct_field_by_name(T, INTERNAL_VARIADIC_FLAG)
+		} else {
+			return Parse_Error {
+				.Extra_Positional,
+				fmt.tprintf("Got extra positional argument `%s` with nowhere to store it.", arg),
+			}
+		}
+	}
+
+	ptr := cast(rawptr)(cast(uintptr)model + field.offset)
+	args_tag, _ := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
+	field_name := get_field_name(field)
+	error = parse_and_set_pointer_by_type(ptr, arg, field.type, args_tag)
+	#partial switch &specific_error in error {
+	case Parse_Error:
+		specific_error.message = fmt.tprintf("Unable to set positional #%i (%s) of type %v to `%s`.%s%s",
+			pos,
+			field_name,
+			field.type,
+			arg,
+			" " if len(specific_error.message) > 0 else "",
+			specific_error.message)
+	case nil:
+		bit_array.set(&parser.filled_pos, pos)
+		bit_array.set(&parser.fields_set, index)
+	}
+
+	return
+}
+
+register_field :: proc(parser: ^Parser, field: reflect.Struct_Field, index: int) {
+	if pos, ok := get_field_pos(field); ok {
+		bit_array.set(&parser.filled_pos, pos)
+	}
+
+	bit_array.set(&parser.fields_set, index)
+}
+
+// Set a `-flag` argument, Odin-style.
+@(optimization_mode="size")
+set_odin_flag :: proc(model: ^$T, parser: ^Parser, name: string) -> (error: Error) {
+	// We make a special case for help requests.
+	switch name {
+	case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
+		return Help_Request{}
+	}
+
+	field, index := get_field_by_name(model, name) or_return
+
+	#partial switch specific_type_info in field.type.variant {
+	case runtime.Type_Info_Boolean:
+		ptr := cast(^bool)(cast(uintptr)model + field.offset)
+		ptr^ = true
+	case:
+		return Parse_Error {
+			.Bad_Value,
+			fmt.tprintf("Unable to set `%s` of type %v to true.", name, field.type),
+		}
+	}
+
+	register_field(parser, field, index)
+	return
+}
+
+// Set a `-flag` argument, UNIX-style.
+@(optimization_mode="size")
+set_unix_flag :: proc(model: ^$T, parser: ^Parser, name: string) -> (future_args: int, error: Error) {
+	// We make a special case for help requests.
+	switch name {
+	case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
+		return 0, Help_Request{}
+	}
+
+	field, index := get_field_by_name(model, name) or_return
+
+	#partial switch specific_type_info in field.type.variant {
+	case runtime.Type_Info_Boolean:
+		ptr := cast(^bool)(cast(uintptr)model + field.offset)
+		ptr^ = true
+	case runtime.Type_Info_Dynamic_Array:
+		future_args = 1
+		if tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
+			if length, is_variadic := get_struct_subtag(tag, SUBTAG_VARIADIC); is_variadic {
+				// Variadic arrays may specify how many arguments they consume at once.
+				// Otherwise, they take everything that's left.
+				if value, value_ok := strconv.parse_u64_of_base(length, 10); value_ok {
+					future_args = cast(int)value
+				} else {
+					future_args = max(int)
+				}
+			}
+		}
+	case:
+		// `--flag`, waiting on its value.
+		future_args = 1
+	}
+
+	register_field(parser, field, index)
+	return
+}
+
+// Set a `-flag:option` argument.
+@(optimization_mode="size")
+set_option :: proc(model: ^$T, parser: ^Parser, name, option: string) -> (error: Error) {
+	field, index := get_field_by_name(model, name) or_return
+
+	if len(option) == 0 {
+		return Parse_Error {
+			.No_Value,
+			fmt.tprintf("Setting `%s` to an empty value is meaningless.", name),
+		}
+	}
+
+	// Guard against incorrect syntax.
+	#partial switch specific_type_info in field.type.variant {
+	case runtime.Type_Info_Map:
+		return Parse_Error {
+			.No_Value,
+			fmt.tprintf("Unable to set `%s` of type %v to `%s`. Are you missing an `=`? The correct format is `map:key=value`.", name, field.type, option),
+		}
+	}
+
+	ptr := cast(rawptr)(cast(uintptr)model + field.offset)
+	args_tag := reflect.struct_tag_get(field.tag, TAG_ARGS)
+	error = parse_and_set_pointer_by_type(ptr, option, field.type, args_tag)
+	#partial switch &specific_error in error {
+	case Parse_Error:
+		specific_error.message = fmt.tprintf("Unable to set `%s` of type %v to `%s`.%s%s",
+			name,
+			field.type,
+			option,
+			" " if len(specific_error.message) > 0 else "",
+			specific_error.message)
+	case nil:
+		register_field(parser, field, index)
+	}
+
+	return
+}
+
+// Set a `-map:key=value` argument.
+@(optimization_mode="size")
+set_key_value :: proc(model: ^$T, parser: ^Parser, name, key, value: string) -> (error: Error) {
+	field, index := get_field_by_name(model, name) or_return
+
+	#partial switch specific_type_info in field.type.variant {
+	case runtime.Type_Info_Map:
+		key := key
+		key_ptr := cast(rawptr)&key
+		key_cstr: cstring
+		if reflect.is_cstring(specific_type_info.key) {
+			// We clone the key here, because it's liable to be a slice of an
+			// Odin string, and we need to put a NUL terminator in it.
+			key_cstr = strings.clone_to_cstring(key)
+			key_ptr = &key_cstr
+		}
+		defer if key_cstr != nil {
+			delete(key_cstr)
+		}
+
+		raw_map := (^runtime.Raw_Map)(cast(uintptr)model + field.offset)
+
+		hash := specific_type_info.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,
+				specific_type_info.map_info,
+				hash,
+				key_ptr,
+			)
+		}
+
+		if value_ptr == nil {
+			alloc_error: runtime.Allocator_Error = ---
+			elem_backing, alloc_error = mem.alloc_bytes(specific_type_info.value.size, specific_type_info.value.align)
+			if elem_backing == nil {
+				return Parse_Error {
+					alloc_error,
+					"Failed to allocate element backing for map value.",
+				}
+			}
+
+			backing_alloc = true
+			value_ptr = raw_data(elem_backing)
+		}
+
+		args_tag, _ := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
+		error = parse_and_set_pointer_by_type(value_ptr, value, specific_type_info.value, args_tag)
+		#partial switch &specific_error in error {
+		case Parse_Error:
+			specific_error.message = fmt.tprintf("Unable to set `%s` of type %v with key=value: `%s`=`%s`.%s%s",
+				name,
+				field.type,
+				key,
+				value,
+				" " if len(specific_error.message) > 0 else "",
+				specific_error.message)
+		}
+
+		if backing_alloc {
+			runtime.__dynamic_map_set(raw_map,
+				specific_type_info.map_info,
+				hash,
+				key_ptr,
+				value_ptr,
+			)
+
+			delete(elem_backing)
+		}
+
+		register_field(parser, field, index)
+		return
+	}
+
+	return Parse_Error {
+		.Bad_Value,
+		fmt.tprintf("Unable to set `%s` of type %v with key=value: `%s`=`%s`.", name, field.type, key, value),
+	}
+}

+ 162 - 0
core/flags/internal_parsing.odin

@@ -0,0 +1,162 @@
+//+private
+package flags
+
+import "core:container/bit_array"
+import "core:strconv"
+import "core:strings"
+
+// Used to group state together.
+Parser :: struct {
+	// `fields_set` tracks which arguments have been set.
+	// It uses their struct field index.
+	fields_set: bit_array.Bit_Array,
+
+	// `filled_pos` tracks which arguments have been filled into positional
+	// spots, much like how `fmt` treats them.
+	filled_pos: bit_array.Bit_Array,
+}
+
+parse_one_odin_arg :: proc(model: ^$T, parser: ^Parser, arg: string) -> (error: Error) {
+	arg := arg
+
+	if strings.has_prefix(arg, "-") {
+		arg = arg[1:]
+
+		flag: string
+		assignment_rune: rune
+		find_assignment: for r, i in arg {
+			switch r {
+			case ':', '=':
+				assignment_rune = r
+				flag = arg[:i]
+				arg = arg[1 + i:]
+				break find_assignment
+			case:
+				continue find_assignment
+			}
+		}
+
+		if assignment_rune == 0 {
+			if len(arg) == 0 {
+				return Parse_Error {
+					.No_Flag,
+					"No flag was given.",
+				}
+			}
+
+			// -flag
+			set_odin_flag(model, parser, arg) or_return
+
+		} else if assignment_rune == ':' {
+			// -flag:option <OR> -map:key=value
+			error = set_option(model, parser, flag, arg)
+
+			if error != nil {
+				// -flag:option did not work, so this may be a -map:key=value set.
+				find_equals: for r, i in arg {
+					if r == '=' {
+						key := arg[:i]
+						arg = arg[1 + i:]
+						error = set_key_value(model, parser, flag, key, arg)
+						break find_equals
+					}
+				}
+			}
+
+		} else {
+			// -flag=option, alternative syntax
+			set_option(model, parser, flag, arg) or_return
+		}
+
+	} else {
+		// positional
+		error = push_positional(model, parser, arg)
+	}
+
+	return
+}
+
+parse_one_unix_arg :: proc(model: ^$T, parser: ^Parser, arg: string) -> (
+	future_args: int,
+	current_flag: string,
+	error: Error,
+) {
+	arg := arg
+
+	if strings.has_prefix(arg, "-") {
+		// -flag
+		arg = arg[1:]
+
+		if strings.has_prefix(arg, "-") {
+			// Allow `--` to function as `-`.
+			arg = arg[1:]
+		}
+
+		flag: string
+		find_assignment: for r, i in arg {
+			if r == '=' {
+				// --flag=option
+				flag = arg[:i]
+				arg = arg[1 + i:]
+				error = set_option(model, parser, flag, arg)
+				return
+			}
+		}
+
+		// --flag option, potentially
+		future_args = set_unix_flag(model, parser, arg) or_return
+		current_flag = arg
+
+	} else {
+		// positional
+		error = push_positional(model, parser, arg)
+	}
+
+	return
+}
+
+// Parse a number of requirements specifier.
+//
+// Examples:
+//
+//    `min`
+//    `<max`
+//    `min<max`
+parse_requirements :: proc(str: string) -> (minimum, maximum: int, ok: bool) {
+	if len(str) == 0 {
+		return 1, max(int), true
+	}
+
+	if less_than := strings.index_byte(str, '<'); less_than != -1 {
+		if len(str) == 1 {
+			return 0, 0, false
+		}
+
+		#no_bounds_check left  := str[:less_than]
+		#no_bounds_check right := str[1 + less_than:]
+
+		if left_value, parse_ok := strconv.parse_u64_of_base(left, 10); parse_ok {
+			minimum = cast(int)left_value
+		} else if len(left) > 0 {
+			return 0, 0, false
+		}
+
+		if right_value, parse_ok := strconv.parse_u64_of_base(right, 10); parse_ok {
+			maximum = cast(int)right_value
+		} else if len(right) > 0 {
+			return 0, 0, false
+		} else {
+			maximum = max(int)
+		}
+	} else {
+		if value, parse_ok := strconv.parse_u64_of_base(str, 10); parse_ok {
+			minimum = cast(int)value
+			maximum = max(int)
+		} else {
+			return 0, 0, false
+		}
+	}
+
+	ok = true
+	return
+}

+ 548 - 0
core/flags/internal_rtti.odin

@@ -0,0 +1,548 @@
+//+private
+package flags
+
+import "base:intrinsics"
+import "base:runtime"
+import "core:fmt"
+import "core:mem"
+@require import "core:net"
+import "core:os"
+import "core:reflect"
+import "core:strconv"
+import "core:strings"
+@require import "core:time"
+@require import "core:time/datetime"
+import "core:unicode/utf8"
+
+@(optimization_mode="size")
+parse_and_set_pointer_by_base_type :: proc(ptr: rawptr, str: string, type_info: ^runtime.Type_Info) -> bool {
+	bounded_int :: proc(value, min, max: i128) -> (result: i128, ok: bool) {
+		return value, min <= value && value <= max
+	}
+
+	bounded_uint :: proc(value, max: u128) -> (result: u128, ok: bool) {
+		return value, value <= max
+	}
+
+	// NOTE(Feoramund): This procedure has been written with the goal in mind
+	// of generating the least amount of assembly, given that this library is
+	// likely to be called once and forgotten.
+	//
+	// I've rewritten the switch tables below in 3 different ways, and the
+	// current one generates the least amount of code for me on Linux AMD64.
+	//
+	// The other two ways were:
+	//
+	// - the original implementation: use of parametric polymorphism which led
+	//   to dozens of functions generated, one for each type.
+	//
+	// - a `value, ok` assignment statement with the `or_return` done at the
+	//   end of the switch, instead of inline.
+	//
+	// This seems to be the smallest way for now.
+
+	#partial switch specific_type_info in type_info.variant {
+	case runtime.Type_Info_Integer:
+		if specific_type_info.signed {
+			value := strconv.parse_i128(str) or_return
+			switch type_info.id {
+				case i8:     (cast(^i8)    ptr)^ = cast(i8)     bounded_int(value, cast(i128)min(i8),     cast(i128)max(i8)    ) or_return
+				case i16:    (cast(^i16)   ptr)^ = cast(i16)    bounded_int(value, cast(i128)min(i16),    cast(i128)max(i16)   ) or_return
+				case i32:    (cast(^i32)   ptr)^ = cast(i32)    bounded_int(value, cast(i128)min(i32),    cast(i128)max(i32)   ) or_return
+				case i64:    (cast(^i64)   ptr)^ = cast(i64)    bounded_int(value, cast(i128)min(i64),    cast(i128)max(i64)   ) or_return
+				case i128:   (cast(^i128)  ptr)^ = value
+
+				case int:    (cast(^int)   ptr)^ = cast(int)    bounded_int(value, cast(i128)min(int),    cast(i128)max(int)   ) or_return
+
+				case i16le:  (cast(^i16le) ptr)^ = cast(i16le)  bounded_int(value, cast(i128)min(i16le),  cast(i128)max(i16le) ) or_return
+				case i32le:  (cast(^i32le) ptr)^ = cast(i32le)  bounded_int(value, cast(i128)min(i32le),  cast(i128)max(i32le) ) or_return
+				case i64le:  (cast(^i64le) ptr)^ = cast(i64le)  bounded_int(value, cast(i128)min(i64le),  cast(i128)max(i64le) ) or_return
+				case i128le: (cast(^i128le)ptr)^ = cast(i128le) bounded_int(value, cast(i128)min(i128le), cast(i128)max(i128le)) or_return
+
+				case i16be:  (cast(^i16be) ptr)^ = cast(i16be)  bounded_int(value, cast(i128)min(i16be),  cast(i128)max(i16be) ) or_return
+				case i32be:  (cast(^i32be) ptr)^ = cast(i32be)  bounded_int(value, cast(i128)min(i32be),  cast(i128)max(i32be) ) or_return
+				case i64be:  (cast(^i64be) ptr)^ = cast(i64be)  bounded_int(value, cast(i128)min(i64be),  cast(i128)max(i64be) ) or_return
+				case i128be: (cast(^i128be)ptr)^ = cast(i128be) bounded_int(value, cast(i128)min(i128be), cast(i128)max(i128be)) or_return
+			}
+		} else {
+			value := strconv.parse_u128(str) or_return
+			switch type_info.id {
+				case u8:      (cast(^u8)     ptr)^ = cast(u8)      bounded_uint(value, cast(u128)max(u8)     ) or_return
+				case u16:     (cast(^u16)    ptr)^ = cast(u16)     bounded_uint(value, cast(u128)max(u16)    ) or_return
+				case u32:     (cast(^u32)    ptr)^ = cast(u32)     bounded_uint(value, cast(u128)max(u32)    ) or_return
+				case u64:     (cast(^u64)    ptr)^ = cast(u64)     bounded_uint(value, cast(u128)max(u64)    ) or_return
+				case u128:    (cast(^u128)   ptr)^ = value
+
+				case uint:    (cast(^uint)   ptr)^ = cast(uint)    bounded_uint(value, cast(u128)max(uint)   ) or_return
+				case uintptr: (cast(^uintptr)ptr)^ = cast(uintptr) bounded_uint(value, cast(u128)max(uintptr)) or_return
+
+				case u16le:   (cast(^u16le)  ptr)^ = cast(u16le)   bounded_uint(value, cast(u128)max(u16le)  ) or_return
+				case u32le:   (cast(^u32le)  ptr)^ = cast(u32le)   bounded_uint(value, cast(u128)max(u32le)  ) or_return
+				case u64le:   (cast(^u64le)  ptr)^ = cast(u64le)   bounded_uint(value, cast(u128)max(u64le)  ) or_return
+				case u128le:  (cast(^u128le) ptr)^ = cast(u128le)  bounded_uint(value, cast(u128)max(u128le) ) or_return
+
+				case u16be:   (cast(^u16be)  ptr)^ = cast(u16be)   bounded_uint(value, cast(u128)max(u16be)  ) or_return
+				case u32be:   (cast(^u32be)  ptr)^ = cast(u32be)   bounded_uint(value, cast(u128)max(u32be)  ) or_return
+				case u64be:   (cast(^u64be)  ptr)^ = cast(u64be)   bounded_uint(value, cast(u128)max(u64be)  ) or_return
+				case u128be:  (cast(^u128be) ptr)^ = cast(u128be)  bounded_uint(value, cast(u128)max(u128be) ) or_return
+			}
+		}
+
+	case runtime.Type_Info_Rune:
+		if utf8.rune_count_in_string(str) != 1 {
+			return false
+		}
+
+		(cast(^rune)ptr)^ = utf8.rune_at_pos(str, 0)
+
+	case runtime.Type_Info_Float:
+		value := strconv.parse_f64(str) or_return
+		switch type_info.id {
+			case f16:   (cast(^f16)  ptr)^ = cast(f16)   value
+			case f32:   (cast(^f32)  ptr)^ = cast(f32)   value
+			case f64:   (cast(^f64)  ptr)^ =             value
+
+			case f16le: (cast(^f16le)ptr)^ = cast(f16le) value
+			case f32le: (cast(^f32le)ptr)^ = cast(f32le) value
+			case f64le: (cast(^f64le)ptr)^ = cast(f64le) value
+
+			case f16be: (cast(^f16be)ptr)^ = cast(f16be) value
+			case f32be: (cast(^f32be)ptr)^ = cast(f32be) value
+			case f64be: (cast(^f64be)ptr)^ = cast(f64be) value
+		}
+	
+	case runtime.Type_Info_Complex:
+		value := strconv.parse_complex128(str) or_return
+		switch type_info.id {
+			case complex128: (cast(^complex128)ptr)^ = value
+			case complex64:  (cast(^complex64) ptr)^ = cast(complex64)value
+			case complex32:  (cast(^complex32) ptr)^ = cast(complex32)value
+		}
+	
+	case runtime.Type_Info_Quaternion:
+		value := strconv.parse_quaternion256(str) or_return
+		switch type_info.id {
+			case quaternion256: (cast(^quaternion256)ptr)^ = value
+			case quaternion128: (cast(^quaternion128)ptr)^ = cast(quaternion128)value
+			case quaternion64:  (cast(^quaternion64) ptr)^ = cast(quaternion64)value
+		}
+
+	case runtime.Type_Info_String:
+		if specific_type_info.is_cstring {
+			cstr_ptr := cast(^cstring)ptr
+			if cstr_ptr != nil {
+				// Prevent memory leaks from us setting this value multiple times.
+				delete(cstr_ptr^)
+			}
+			cstr_ptr^ = strings.clone_to_cstring(str)
+		} else {
+			(cast(^string)ptr)^ = str
+		}
+
+	case runtime.Type_Info_Boolean:
+		value := strconv.parse_bool(str) or_return
+		switch type_info.id {
+			case bool: (cast(^bool) ptr)^ =           value
+			case b8:   (cast(^b8)   ptr)^ = cast(b8)  value
+			case b16:  (cast(^b16)  ptr)^ = cast(b16) value
+			case b32:  (cast(^b32)  ptr)^ = cast(b32) value
+			case b64:  (cast(^b64)  ptr)^ = cast(b64) value
+		}
+
+	case runtime.Type_Info_Bit_Set:
+		// Parse a string of 1's and 0's, from left to right,
+		// least significant bit to most significant bit.
+		value: u128
+
+		// NOTE: `upper` is inclusive, i.e: `0..=31`
+		max_bit_index := cast(u128)(1 + specific_type_info.upper - specific_type_info.lower)
+		bit_index : u128 = 0
+		#no_bounds_check for string_index : uint = 0; string_index < len(str); string_index += 1 {
+			if bit_index == max_bit_index {
+				// The string's too long for this bit_set.
+				return false
+			}
+
+			switch str[string_index] {
+			case '1':
+				value |= 1 << bit_index
+				bit_index += 1
+			case '0':
+				bit_index += 1
+				continue
+			case '_':
+				continue
+			case:
+				return false
+			}
+		}
+
+		if specific_type_info.underlying != nil {
+			set_unbounded_integer_by_type(ptr, value, specific_type_info.underlying.id)
+		} else {
+			switch 8*type_info.size {
+			case 8:   (cast(^u8)   ptr)^ = cast(u8)   value
+			case 16:  (cast(^u16)  ptr)^ = cast(u16)  value
+			case 32:  (cast(^u32)  ptr)^ = cast(u32)  value
+			case 64:  (cast(^u64)  ptr)^ = cast(u64)  value
+			case 128: (cast(^u128) ptr)^ = cast(u128) value
+			}
+		}
+
+	case:
+		fmt.panicf("Unsupported base data type: %v", specific_type_info)
+	}
+
+	return true
+}
+
+// This proc exists to make error handling easier, since everything in the base
+// type one above works on booleans. It's a simple parsing error if it's false.
+//
+// However, here we have to be more careful about how we handle errors,
+// especially with files.
+//
+// We want to provide as informative as an error as we can.
+@(optimization_mode="size", disabled=NO_CORE_NAMED_TYPES)
+parse_and_set_pointer_by_named_type :: proc(ptr: rawptr, str: string, data_type: typeid, arg_tag: string, out_error: ^Error) {
+	// Core types currently supported:
+	//
+	// - os.Handle
+	// - time.Time
+	// - datetime.DateTime
+	// - net.Host_Or_Endpoint
+
+	GENERIC_RFC_3339_ERROR :: "Invalid RFC 3339 string. Try this format: `yyyy-mm-ddThh:mm:ssZ`, for example `2024-02-29T16:30:00Z`."
+
+	out_error^ = nil
+
+	if data_type == os.Handle {
+		// NOTE: `os` is hopefully available everywhere, even if it might panic on some calls.
+		wants_read := false
+		wants_write := false
+		mode: int
+
+		if file, ok := get_struct_subtag(arg_tag, SUBTAG_FILE); ok {
+			for i := 0; i < len(file); i += 1 {
+				#no_bounds_check switch file[i] {
+				case 'r': wants_read = true
+				case 'w': wants_write = true
+				case 'c': mode |= os.O_CREATE
+				case 'a': mode |= os.O_APPEND
+				case 't': mode |= os.O_TRUNC
+				}
+			}
+		}
+
+		// Sane default.
+		// owner/group/other: r--r--r--
+		perms: int = 0o444
+
+		if wants_read && wants_write {
+			mode |= os.O_RDWR
+			perms |= 0o200
+		} else if wants_write {
+			mode |= os.O_WRONLY
+			perms |= 0o200
+		} else {
+			mode |= os.O_RDONLY
+		}
+
+		if permstr, ok := get_struct_subtag(arg_tag, SUBTAG_PERMS); ok {
+			if value, parse_ok := strconv.parse_u64_of_base(permstr, 8); parse_ok {
+				perms = cast(int)value
+			}
+		}
+
+		handle, errno := os.open(str, mode, perms)
+		if errno != 0 {
+			// NOTE(Feoramund): os.Errno is system-dependent, and there's
+			// currently no good way to translate them all into strings.
+			//
+			// The upcoming `os2` package will hopefully solve this.
+			//
+			// We can at least provide the number for now, so the user can look
+			// it up.
+			out_error^ = Open_File_Error {
+				str,
+				errno,
+				mode,
+				perms,
+			}
+			return
+		}
+
+		(cast(^os.Handle)ptr)^ = handle
+		return
+	}
+
+	when IMPORTING_TIME {
+		if data_type == time.Time {
+			// NOTE: The leap second data is discarded.
+			res, consumed := time.rfc3339_to_time_utc(str)
+			if consumed == 0 {
+				// The RFC 3339 parsing facilities provide no indication as to what
+				// went wrong, so just treat it as a regular parsing error.
+				out_error^ = Parse_Error {
+					.Bad_Value,
+					GENERIC_RFC_3339_ERROR,
+				}
+				return
+			}
+
+			(cast(^time.Time)ptr)^ = res
+			return
+		} else if data_type == datetime.DateTime {
+			// NOTE: The UTC offset and leap second data are discarded.
+			res, _, _, consumed := time.rfc3339_to_components(str)
+			if consumed == 0 {
+				out_error^ = Parse_Error {
+					.Bad_Value,
+					GENERIC_RFC_3339_ERROR,
+				}
+				return
+			}
+
+			(cast(^datetime.DateTime)ptr)^ = res
+			return
+		}
+	}
+
+	when IMPORTING_NET {
+		if data_type == net.Host_Or_Endpoint {
+			addr, net_error := net.parse_hostname_or_endpoint(str)
+			if net_error != nil {
+				// We pass along `net.Error` here.
+				out_error^ = Parse_Error {
+					net_error,
+					"Invalid Host/Endpoint.",
+				}
+				return
+			}
+
+			(cast(^net.Host_Or_Endpoint)ptr)^ = addr
+			return
+		}
+	}
+
+	out_error ^= Parse_Error {
+		// The caller will add more details.
+		.Unsupported_Type,
+		"",
+	}
+}
+
+@(optimization_mode="size")
+set_unbounded_integer_by_type :: proc(ptr: rawptr, value: $T, data_type: typeid) where intrinsics.type_is_integer(T) {
+	switch data_type {
+	case i8:      (cast(^i8)     ptr)^ = cast(i8)      value
+	case i16:     (cast(^i16)    ptr)^ = cast(i16)     value
+	case i32:     (cast(^i32)    ptr)^ = cast(i32)     value
+	case i64:     (cast(^i64)    ptr)^ = cast(i64)     value
+	case i128:    (cast(^i128)   ptr)^ = cast(i128)    value
+
+	case int:     (cast(^int)    ptr)^ = cast(int)     value
+
+	case i16le:   (cast(^i16le)  ptr)^ = cast(i16le)   value
+	case i32le:   (cast(^i32le)  ptr)^ = cast(i32le)   value
+	case i64le:   (cast(^i64le)  ptr)^ = cast(i64le)   value
+	case i128le:  (cast(^i128le) ptr)^ = cast(i128le)  value
+
+	case i16be:   (cast(^i16be)  ptr)^ = cast(i16be)   value
+	case i32be:   (cast(^i32be)  ptr)^ = cast(i32be)   value
+	case i64be:   (cast(^i64be)  ptr)^ = cast(i64be)   value
+	case i128be:  (cast(^i128be) ptr)^ = cast(i128be)  value
+
+	case u8:      (cast(^u8)     ptr)^ = cast(u8)      value
+	case u16:     (cast(^u16)    ptr)^ = cast(u16)     value
+	case u32:     (cast(^u32)    ptr)^ = cast(u32)     value
+	case u64:     (cast(^u64)    ptr)^ = cast(u64)     value
+	case u128:    (cast(^u128)   ptr)^ = cast(u128)    value
+
+	case uint:    (cast(^uint)   ptr)^ = cast(uint)    value
+	case uintptr: (cast(^uintptr)ptr)^ = cast(uintptr) value
+
+	case u16le:   (cast(^u16le)  ptr)^ = cast(u16le)   value
+	case u32le:   (cast(^u32le)  ptr)^ = cast(u32le)   value
+	case u64le:   (cast(^u64le)  ptr)^ = cast(u64le)   value
+	case u128le:  (cast(^u128le) ptr)^ = cast(u128le)  value
+
+	case u16be:   (cast(^u16be)  ptr)^ = cast(u16be)   value
+	case u32be:   (cast(^u32be)  ptr)^ = cast(u32be)   value
+	case u64be:   (cast(^u64be)  ptr)^ = cast(u64be)   value
+	case u128be:  (cast(^u128be) ptr)^ = cast(u128be)  value
+
+	case rune:    (cast(^rune)   ptr)^ = cast(rune)    value
+
+	case:
+		fmt.panicf("Unsupported integer backing type: %v", data_type)
+	}
+}
+
+@(optimization_mode="size")
+parse_and_set_pointer_by_type :: proc(ptr: rawptr, str: string, type_info: ^runtime.Type_Info, arg_tag: string) -> (error: Error) {
+	#partial switch specific_type_info in type_info.variant {
+	case runtime.Type_Info_Named:
+		if global_custom_type_setter != nil {
+			// The program gets to go first.
+			error_message, handled, alloc_error := global_custom_type_setter(ptr, type_info.id, str, arg_tag)
+
+			if alloc_error != nil {
+				// There was an allocation error. Bail out.
+				return Parse_Error {
+					alloc_error,
+					"Custom type setter encountered allocation error.",
+				}
+			}
+
+			if handled {
+				// The program handled the type.
+
+				if len(error_message) != 0 {
+					// However, there was an error. Pass it along.
+					error = Parse_Error {
+						.Bad_Value,
+						error_message,
+					}
+				}
+
+				return
+			}
+		}
+
+		// Might be a named enum. Need to check here first, since we handle all enums.
+		if enum_type_info, is_enum := specific_type_info.base.variant.(runtime.Type_Info_Enum); is_enum {
+			if value, ok := reflect.enum_from_name_any(type_info.id, str); ok {
+				set_unbounded_integer_by_type(ptr, value, enum_type_info.base.id)
+			} else {
+				return Parse_Error {
+					.Bad_Value,
+					fmt.tprintf("Invalid value name. Valid names are: %s", enum_type_info.names),
+				}
+			}
+		} else {
+			parse_and_set_pointer_by_named_type(ptr, str, type_info.id, arg_tag, &error)
+			
+			if error != nil {
+				// So far, it's none of the types that we recognize.
+				// Check to see if we can set it by base type, if allowed.
+				if _, is_indistinct := get_struct_subtag(arg_tag, SUBTAG_INDISTINCT); is_indistinct {
+					return parse_and_set_pointer_by_type(ptr, str, specific_type_info.base, arg_tag)
+				}
+			}
+		}
+
+	case runtime.Type_Info_Dynamic_Array:
+		ptr := cast(^runtime.Raw_Dynamic_Array)ptr
+
+		// Try to convert the value first.
+		elem_backing, alloc_error := mem.alloc_bytes(specific_type_info.elem.size, specific_type_info.elem.align)
+		if alloc_error != nil {
+			return Parse_Error {
+				alloc_error,
+				"Failed to allocate element backing for dynamic array.",
+			}
+		}
+		defer delete(elem_backing)
+		parse_and_set_pointer_by_type(raw_data(elem_backing), str, specific_type_info.elem, arg_tag) or_return
+
+		if !runtime.__dynamic_array_resize(ptr, specific_type_info.elem.size, specific_type_info.elem.align, ptr.len + 1) {
+			// NOTE: This is purely an assumption that it's OOM.
+			// Regardless, the resize failed.
+			return Parse_Error {
+				runtime.Allocator_Error.Out_Of_Memory,
+				"Failed to resize dynamic array.",
+			}
+		}
+
+		subptr := cast(rawptr)(
+			cast(uintptr)ptr.data +
+			cast(uintptr)((ptr.len - 1) * specific_type_info.elem.size))
+		mem.copy(subptr, raw_data(elem_backing), len(elem_backing))
+
+	case runtime.Type_Info_Enum:
+		// This is a nameless enum.
+		// The code here is virtually the same as above for named enums.
+		if value, ok := reflect.enum_from_name_any(type_info.id, str); ok {
+			set_unbounded_integer_by_type(ptr, value, specific_type_info.base.id)
+		} else {
+			return Parse_Error {
+				.Bad_Value,
+				fmt.tprintf("Invalid value name. Valid names are: %s", specific_type_info.names),
+			}
+		}
+
+	case:
+		if !parse_and_set_pointer_by_base_type(ptr, str, type_info) {
+			return Parse_Error {
+				// The caller will add more details.
+				.Bad_Value,
+				"",
+			}
+		}
+	}
+
+	return
+}
+
+get_struct_subtag :: get_subtag
+
+get_field_name :: proc(field: reflect.Struct_Field) -> string {
+	if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
+		if name_subtag, name_ok := get_struct_subtag(args_tag, SUBTAG_NAME); name_ok {
+			return name_subtag
+		}
+	}
+
+	name, _ := strings.replace_all(field.name, "_", "-", context.temp_allocator)
+	return name
+}
+
+get_field_pos :: proc(field: reflect.Struct_Field) -> (int, bool) {
+	if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
+		if pos_subtag, pos_ok := get_struct_subtag(args_tag, SUBTAG_POS); pos_ok {
+			if value, parse_ok := strconv.parse_u64_of_base(pos_subtag, 10); parse_ok {
+				return cast(int)value, true
+			}
+		}
+	}
+
+	return 0, false
+}
+
+// Get a struct field by its field name or `name` subtag.
+get_field_by_name :: proc(model: ^$T, name: string) -> (result: reflect.Struct_Field, index: int, error: Error) {
+	for field, i in reflect.struct_fields_zipped(T) {
+		if get_field_name(field) == name {
+			return field, i, nil
+		}
+	}
+
+	error = Parse_Error {
+		.Missing_Flag,
+		fmt.tprintf("Unable to find any flag named `%s`.", name),
+	}
+	return
+}
+
+// Get a struct field by its `pos` subtag.
+get_field_by_pos :: proc(model: ^$T, pos: int) -> (result: reflect.Struct_Field, index: int, ok: bool) {
+	for field, i in reflect.struct_fields_zipped(T) {
+		args_tag, tag_ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
+		if !tag_ok {
+			continue
+		}
+
+		pos_subtag, pos_ok := get_struct_subtag(args_tag, SUBTAG_POS)
+		if !pos_ok {
+			continue
+		}
+
+		value, parse_ok := strconv.parse_u64_of_base(pos_subtag, 10)
+		if parse_ok && cast(int)value == pos {
+			return field, i, true
+		}
+	}
+
+	return
+}

+ 243 - 0
core/flags/internal_validation.odin

@@ -0,0 +1,243 @@
+//+private
+package flags
+
+import "base:runtime"
+import "core:container/bit_array"
+import "core:fmt"
+import "core:mem"
+import "core:os"
+import "core:reflect"
+import "core:strconv"
+import "core:strings"
+
+// This proc is used to assert that `T` meets the expectations of the library.
+@(optimization_mode="size", disabled=ODIN_DISABLE_ASSERT)
+validate_structure :: proc(model_type: $T, style: Parsing_Style, loc := #caller_location) {
+	positionals_assigned_so_far: bit_array.Bit_Array
+
+	check_fields: for field in reflect.struct_fields_zipped(T) {
+		if style == .Unix {
+			#partial switch specific_type_info in field.type.variant {
+			case runtime.Type_Info_Map:
+				fmt.panicf("%T.%s is a map type, and these are not supported in UNIX-style parsing mode.",
+					model_type, field.name, loc = loc)
+			}
+		}
+
+		name_is_safe := true
+		defer {
+			fmt.assertf(name_is_safe, "%T.%s is using a reserved name.",
+				model_type, field.name, loc = loc)
+		}
+
+		switch field.name {
+		case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
+			name_is_safe = false
+		}
+
+		args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
+		if !ok {
+			// If it has no args tag, then we've checked all we need to.
+			// Most of this proc is validating that the subtags are sane.
+			continue
+		}
+
+		if name, has_name := get_struct_subtag(args_tag, SUBTAG_NAME); has_name {
+			fmt.assertf(len(name) > 0, "%T.%s has a zero-length `%s`.",
+				model_type, field.name, SUBTAG_NAME, loc = loc)
+
+			fmt.assertf(strings.index(name, " ") == -1, "%T.%s has a `%s` with spaces in it.",
+				model_type, field.name, SUBTAG_NAME, loc = loc)
+
+			switch name {
+			case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
+				name_is_safe = false
+				continue check_fields
+			case:
+				name_is_safe = true
+			}
+		}
+
+		if pos_str, has_pos := get_struct_subtag(args_tag, SUBTAG_POS); has_pos {
+			#partial switch specific_type_info in field.type.variant {
+			case runtime.Type_Info_Map:
+				fmt.panicf("%T.%s has `%s` defined, and this does not make sense on a map type.",
+					model_type, field.name, SUBTAG_POS, loc = loc)
+			}
+
+			pos_value, pos_ok := strconv.parse_u64_of_base(pos_str, 10)
+			fmt.assertf(pos_ok, "%T.%s has `%s` defined as %q but cannot be parsed a base-10 integer >= 0.",
+				model_type, field.name, SUBTAG_POS, pos_str, loc = loc)
+			fmt.assertf(!bit_array.get(&positionals_assigned_so_far, pos_value), "%T.%s has `%s` set to #%i, but that position has already been assigned to another flag.",
+				model_type, field.name, SUBTAG_POS, pos_value, loc = loc)
+			bit_array.set(&positionals_assigned_so_far, pos_value)
+		}
+
+		required_min, required_max: int
+		if requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
+			fmt.assertf(!reflect.is_boolean(field.type), "%T.%s is a required boolean. This is disallowed.",
+				model_type, field.name, loc = loc)
+
+			fmt.assertf(field.name != INTERNAL_VARIADIC_FLAG, "%T.%s is defined as required. This is disallowed.",
+				model_type, field.name, loc = loc)
+
+			if len(requirement) > 0 {
+				if required_min, required_max, ok = parse_requirements(requirement); ok {
+					#partial switch specific_type_info in field.type.variant {
+					case runtime.Type_Info_Dynamic_Array:
+						fmt.assertf(required_min != required_max, "%T.%s has `%s` defined as %q, but the minimum and maximum are the same. Increase the maximum by 1 for an exact number of arguments: (%i<%i)",
+							model_type,
+							field.name,
+							SUBTAG_REQUIRED,
+							requirement,
+							required_min,
+							1 + required_max,
+							loc = loc)
+
+						fmt.assertf(required_min < required_max, "%T.%s has `%s` defined as %q, but the minimum and maximum are swapped.",
+							model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
+
+					case:
+						fmt.panicf("%T.%s has `%s` defined as %q, but ranges are only supported on dynamic arrays.",
+							model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
+					}
+				} else {
+					fmt.panicf("%T.%s has `%s` defined as %q, but it cannot be parsed as a valid range.",
+						model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
+				}
+			}
+		}
+
+		if length, is_variadic := get_struct_subtag(args_tag, SUBTAG_VARIADIC); is_variadic {
+			if value, parse_ok := strconv.parse_u64_of_base(length, 10); parse_ok {
+				fmt.assertf(value > 0,
+					"%T.%s has `%s` set to %i. It must be greater than zero.",
+					model_type, field.name, value, SUBTAG_VARIADIC, loc = loc)
+				fmt.assertf(value != 1,
+					"%T.%s has `%s` set to 1. This has no effect.",
+					model_type, field.name, SUBTAG_VARIADIC, loc = loc)
+			}
+
+			#partial switch specific_type_info in field.type.variant {
+			case runtime.Type_Info_Dynamic_Array:
+				fmt.assertf(style != .Odin,
+					"%T.%s has `%s` defined, but this only makes sense in UNIX-style parsing mode.",
+					model_type, field.name, SUBTAG_VARIADIC, loc = loc)
+			case:
+				fmt.panicf("%T.%s has `%s` defined, but this only makes sense on dynamic arrays.",
+					model_type, field.name, SUBTAG_VARIADIC, loc = loc)
+			}
+		}
+
+		allowed_to_define_file_perms: bool = ---
+		#partial switch specific_type_info in field.type.variant {
+		case runtime.Type_Info_Map:
+			allowed_to_define_file_perms = specific_type_info.value.id == os.Handle
+		case runtime.Type_Info_Dynamic_Array:
+			allowed_to_define_file_perms = specific_type_info.elem.id == os.Handle
+		case:
+			allowed_to_define_file_perms = field.type.id == os.Handle
+		}
+
+		if _, has_file := get_struct_subtag(args_tag, SUBTAG_FILE); has_file {
+			fmt.assertf(allowed_to_define_file_perms, "%T.%s has `%s` defined, but it is not nor does it contain an `os.Handle` type.",
+				model_type, field.name, SUBTAG_FILE, loc = loc)
+		}
+
+		if _, has_perms := get_struct_subtag(args_tag, SUBTAG_PERMS); has_perms {
+			fmt.assertf(allowed_to_define_file_perms, "%T.%s has `%s` defined, but it is not nor does it contain an `os.Handle` type.",
+				model_type, field.name, SUBTAG_PERMS, loc = loc)
+		}
+
+		#partial switch specific_type_info in field.type.variant {
+		case runtime.Type_Info_Map:
+			fmt.assertf(reflect.is_string(specific_type_info.key), "%T.%s is defined as a map[%T]. Only string types are currently supported as map keys.",
+				model_type,
+				field.name,
+				specific_type_info.key)
+		}
+	}
+}
+
+// Validate that all the required arguments are set and that the set arguments
+// are up to the program's expectations.
+@(optimization_mode="size")
+validate_arguments :: proc(model: ^$T, parser: ^Parser) -> Error {
+	check_fields: for field, index in reflect.struct_fields_zipped(T) {
+		was_set := bit_array.get(&parser.fields_set, index)
+
+		field_name := get_field_name(field)
+		args_tag := reflect.struct_tag_get(field.tag, TAG_ARGS)
+		requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED)
+
+		required_min, required_max: int
+		has_requirements: bool
+		if is_required {
+			required_min, required_max, has_requirements = parse_requirements(requirement)
+		}
+
+		if has_requirements && required_min == 0 {
+			// Allow `0<n` or `<n` to bypass the required condition.
+			is_required = false
+		}
+
+		if _, is_array := field.type.variant.(runtime.Type_Info_Dynamic_Array); is_array && has_requirements {
+			// If it's an array, make sure it meets the required number of arguments.
+			ptr := cast(^runtime.Raw_Dynamic_Array)(cast(uintptr)model + field.offset)
+			if required_min == required_max - 1 && ptr.len != required_min {
+				return Validation_Error {
+					fmt.tprintf("The flag `%s` had %i option%s set, but it requires exactly %i.",
+						field_name,
+						ptr.len,
+						"" if ptr.len == 1 else "s",
+						required_min),
+				}
+			} else if required_min > ptr.len || ptr.len >= required_max {
+				if required_max == max(int) {
+					return Validation_Error {
+						fmt.tprintf("The flag `%s` had %i option%s set, but it requires at least %i.",
+							field_name,
+							ptr.len,
+							"" if ptr.len == 1 else "s",
+							required_min),
+					}
+				} else {
+					return Validation_Error {
+						fmt.tprintf("The flag `%s` had %i option%s set, but it requires at least %i and at most %i.",
+							field_name,
+							ptr.len,
+							"" if ptr.len == 1 else "s",
+							required_min,
+							required_max - 1),
+					}
+				}
+			}
+		} else if !was_set {
+			if is_required {
+				return Validation_Error {
+					fmt.tprintf("The required flag `%s` was not set.", field_name),
+				}
+			}
+
+			// Not set, not required; moving on.
+			continue
+		}
+
+		// All default checks have passed. The program gets a look at it now.
+
+		if global_custom_flag_checker != nil {
+			ptr := cast(rawptr)(cast(uintptr)model + field.offset)
+			error := global_custom_flag_checker(model,
+				field.name,
+				mem.make_any(ptr, field.type.id),
+				args_tag)
+
+			if len(error) > 0 {
+				// The program reported an error message.
+				return Validation_Error { error }
+			}
+		}
+	}
+
+	return nil
+}

+ 94 - 0
core/flags/parsing.odin

@@ -0,0 +1,94 @@
+package flags
+
+import "core:container/bit_array"
+
+Parsing_Style :: enum {
+	// Odin-style: `-flag`, `-flag:option`, `-map:key=value`
+	Odin,
+	// UNIX-style: `-flag` or `--flag`, `--flag=argument`, `--flag argument repeating-argument`
+	Unix,
+}
+
+/*
+Parse a slice of command-line arguments into an annotated struct.
+
+*Allocates Using Provided Allocator*
+
+By default, this proc will only allocate memory outside of its lifetime if it
+has to append to a dynamic array, set a map value, or set a cstring.
+
+The program is expected to free any allocations on `model` as a result of parsing.
+
+Inputs:
+- model: A pointer to an annotated struct with flag definitions.
+- args: A slice of strings, usually `os.args[1:]`.
+- style: The argument parsing style.
+- validate_args: If `true`, will ensure that all required arguments are set if no errors occurred.
+- strict: If `true`, will return on first error. Otherwise, parsing continues.
+- allocator: (default: context.allocator)
+- loc: The caller location for debugging purposes (default: #caller_location)
+
+Returns:
+- error: A union of errors; parsing, file open, a help request, or validation.
+*/
+@(optimization_mode="size")
+parse :: proc(
+	model: ^$T,
+	args: []string,
+	style: Parsing_Style = .Odin,
+	validate_args: bool = true,
+	strict: bool = true,
+	allocator := context.allocator,
+	loc := #caller_location,
+) -> (error: Error) {
+	context.allocator = allocator
+	validate_structure(model^, style, loc)
+
+	parser: Parser
+	defer {
+		bit_array.destroy(&parser.filled_pos)
+		bit_array.destroy(&parser.fields_set)
+	}
+
+	switch style {
+	case .Odin:
+		for arg in args {
+			error = parse_one_odin_arg(model, &parser, arg)
+			if strict && error != nil {
+				return
+			}
+		}
+
+	case .Unix:
+		// Support for `-flag argument (repeating-argument ...)`
+		future_args: int
+		current_flag: string
+
+		for i := 0; i < len(args); i += 1 {
+			#no_bounds_check arg := args[i]
+			future_args, current_flag, error = parse_one_unix_arg(model, &parser, arg)
+			if strict && error != nil {
+				return
+			}
+
+			for /**/; future_args > 0; future_args -= 1 {
+				i += 1
+				if i == len(args) {
+					break
+				}
+				#no_bounds_check arg = args[i]
+
+				error = set_option(model, &parser, current_flag, arg)
+				if strict && error != nil {
+					return
+				}
+			}
+		}
+	}
+
+	if error == nil && validate_args {
+		return validate_arguments(model, &parser)
+	}
+
+	return
+}

+ 43 - 0
core/flags/rtti.odin

@@ -0,0 +1,43 @@
+package flags
+
+import "base:runtime"
+
+/*
+Handle setting custom data types.
+
+Inputs:
+- data: A raw pointer to the field where the data will go.
+- data_type: Type information on the underlying field.
+- unparsed_value: The unparsed string that the flag is being set to.
+- args_tag: The `args` tag from the struct's field.
+
+Returns:
+- error: An error message, or an empty string if no error occurred.
+- handled: A boolean indicating if the setter handles this type.
+- alloc_error: If an allocation error occurred, return it here.
+*/
+Custom_Type_Setter :: #type proc(
+	data:           rawptr,
+	data_type:      typeid,
+	unparsed_value: string,
+	args_tag:       string,
+) -> (
+	error:       string,
+	handled:     bool,
+	alloc_error: runtime.Allocator_Error,
+)
+
+@(private)
+global_custom_type_setter: Custom_Type_Setter
+
+/*
+Set the global custom type setter.
+
+Note that only one can be active at a time.
+
+Inputs:
+- setter: The type setter. Pass `nil` to disable any previously set setter.
+*/
+register_type_setter :: proc(setter: Custom_Type_Setter) {
+	global_custom_type_setter = setter
+}

+ 293 - 0
core/flags/usage.odin

@@ -0,0 +1,293 @@
+package flags
+
+import "base:runtime"
+import "core:fmt"
+import "core:io"
+import "core:reflect"
+import "core:slice"
+import "core:strconv"
+import "core:strings"
+
+/*
+Write out the documentation for the command-line arguments to a stream.
+
+Inputs:
+- out: The stream to write to.
+- data_type: The typeid of the data structure to describe.
+- program: The name of the program, usually the first argument to `os.args`.
+- style: The argument parsing style, required to show flags in the proper style.
+*/
+@(optimization_mode="size")
+write_usage :: proc(out: io.Writer, data_type: typeid, program: string = "", style: Parsing_Style = .Odin) {
+	// All flags get their tags parsed so they can be reasoned about later.
+	Flag :: struct {
+		name: string,
+		usage: string,
+		type_description: string,
+		full_length: int,
+		pos: int,
+		required_min, required_max: int,
+		is_positional: bool,
+		is_required: bool,
+		is_boolean: bool,
+		is_variadic: bool,
+		variadic_length: int,
+	}
+
+	//
+	// POSITIONAL+REQUIRED, POSITIONAL, REQUIRED, NON_REQUIRED+NON_POSITIONAL, ...
+	//
+	sort_flags :: proc(i, j: Flag) -> slice.Ordering {
+		// `varg` goes to the end.
+		if i.name == INTERNAL_VARIADIC_FLAG {
+			return .Greater
+		} else if j.name == INTERNAL_VARIADIC_FLAG {
+			return .Less
+		}
+
+		// Handle positionals.
+		if i.is_positional {
+			if j.is_positional {
+				return slice.cmp(i.pos, j.pos)
+			} else {
+				return .Less
+			}
+		} else {
+			if j.is_positional {
+				return .Greater
+			}
+		}
+
+		// Then required flags.
+		if i.is_required {
+			if !j.is_required {
+				return .Less
+			}
+		} else if j.is_required {
+			return .Greater
+		}
+
+		// Finally, sort by name.
+		return slice.cmp(i.name, j.name)
+	}
+
+	describe_array_requirements :: proc(flag: Flag) -> (spec: string) {
+		if flag.is_required {
+			if flag.required_min == flag.required_max - 1 {
+				spec = fmt.tprintf(", exactly %i", flag.required_min)
+			} else if flag.required_min > 0 && flag.required_max == max(int) {
+				spec = fmt.tprintf(", at least %i", flag.required_min)
+			} else if flag.required_min == 0 && flag.required_max > 1 {
+				spec = fmt.tprintf(", at most %i", flag.required_max - 1)
+			} else if flag.required_min > 0 && flag.required_max > 1 {
+				spec = fmt.tprintf(", between %i and %i", flag.required_min, flag.required_max - 1)
+			} else {
+				spec = ", required"
+			}
+		}
+		return
+	}
+
+	builder := strings.builder_make()
+	defer strings.builder_destroy(&builder)
+
+	flag_prefix, flag_assignment: string = ---, ---
+	switch style {
+	case .Odin: flag_prefix = "-";  flag_assignment = ":"
+	case .Unix: flag_prefix = "--"; flag_assignment = " "
+	}
+
+	visible_flags: [dynamic]Flag
+	defer delete(visible_flags)
+
+	longest_flag_length: int
+
+	for field in reflect.struct_fields_zipped(data_type) {
+		flag: Flag
+
+		if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
+			if _, is_hidden := get_struct_subtag(args_tag, SUBTAG_HIDDEN); is_hidden {
+				// Hidden flags stay hidden.
+				continue
+			}
+			if pos_str, is_pos := get_struct_subtag(args_tag, SUBTAG_POS); is_pos {
+				flag.is_positional = true
+				if pos, parse_ok := strconv.parse_u64_of_base(pos_str, 10); parse_ok {
+					flag.pos = cast(int)pos
+				}
+			}
+			if requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
+				flag.is_required = true
+				flag.required_min, flag.required_max, _ = parse_requirements(requirement)
+			}
+			if length_str, is_variadic := get_struct_subtag(args_tag, SUBTAG_VARIADIC); is_variadic {
+				flag.is_variadic = true
+				if length, parse_ok := strconv.parse_u64_of_base(length_str, 10); parse_ok {
+					flag.variadic_length = cast(int)length
+				}
+			}
+		}
+
+		flag.name = get_field_name(field)
+		flag.is_boolean = reflect.is_boolean(field.type)
+
+		if usage, ok := reflect.struct_tag_lookup(field.tag, TAG_USAGE); ok {
+			flag.usage = usage
+		} else {
+			flag.usage = UNDOCUMENTED_FLAG
+		}
+
+		#partial switch specific_type_info in field.type.variant {
+		case runtime.Type_Info_Map:
+			flag.type_description = fmt.tprintf("<%v>=<%v>%s",
+				specific_type_info.key.id,
+				specific_type_info.value.id,
+				", required" if flag.is_required else "")
+
+		case runtime.Type_Info_Dynamic_Array:
+			requirement_spec := describe_array_requirements(flag)
+
+			if flag.is_variadic || flag.name == INTERNAL_VARIADIC_FLAG {
+				if flag.variadic_length == 0 {
+					flag.type_description = fmt.tprintf("<%v, ...>%s",
+						specific_type_info.elem.id,
+						requirement_spec)
+				} else {
+					flag.type_description = fmt.tprintf("<%v, %i at once>%s",
+						specific_type_info.elem.id,
+						flag.variadic_length,
+						requirement_spec)
+				}
+			} else {
+				flag.type_description = fmt.tprintf("<%v>%s", specific_type_info.elem.id,
+					requirement_spec if len(requirement_spec) > 0 else ", multiple")
+			}
+
+		case:
+			if flag.is_boolean {
+				/*
+				if flag.is_required {
+					flag.type_description = ", required"
+				}
+				*/
+			} else {
+				flag.type_description = fmt.tprintf("<%v>%s",
+					field.type.id,
+					", required" if flag.is_required else "")
+			}
+		}
+
+		if flag.name == INTERNAL_VARIADIC_FLAG {
+			flag.full_length = len(flag.type_description)
+		} else if flag.is_boolean {
+			flag.full_length = len(flag_prefix) + len(flag.name) + len(flag.type_description)
+		} else {
+			flag.full_length = len(flag_prefix) + len(flag.name) + len(flag_assignment) + len(flag.type_description)
+		}
+
+		longest_flag_length = max(longest_flag_length, flag.full_length)
+
+		append(&visible_flags, flag)
+	}
+
+	slice.sort_by_cmp(visible_flags[:], sort_flags)
+
+	// All the flags have been figured out now.
+
+	if len(program) > 0 {
+		keep_it_short := len(visible_flags) >= ONE_LINE_FLAG_CUTOFF_COUNT
+
+		strings.write_string(&builder, "Usage:\n\t")
+		strings.write_string(&builder, program)
+
+		for flag in visible_flags {
+			if keep_it_short && !(flag.is_required || flag.is_positional || flag.name == INTERNAL_VARIADIC_FLAG) {
+				continue
+			}
+
+			strings.write_byte(&builder, ' ')
+
+			if flag.name == INTERNAL_VARIADIC_FLAG {
+				strings.write_string(&builder, "...")
+				continue
+			}
+
+			if !flag.is_required { strings.write_byte(&builder, '[') }
+			if !flag.is_positional { strings.write_string(&builder, flag_prefix) }
+			strings.write_string(&builder, flag.name)
+			if !flag.is_required { strings.write_byte(&builder, ']') }
+		}
+
+		strings.write_byte(&builder, '\n')
+	}
+
+	if len(visible_flags) == 0 {
+		// No visible flags. An unusual situation, but prevent any extra work.
+		fmt.wprint(out, strings.to_string(builder))
+		return
+	}
+
+	strings.write_string(&builder, "Flags:\n")
+	
+	// Divide the positional/required arguments and the non-required arguments.
+	divider_index := -1
+	for flag, i in visible_flags {
+		if !flag.is_positional && !flag.is_required {
+			divider_index = i
+			break
+		}
+	}
+	if divider_index == 0 {
+		divider_index = -1
+	}
+
+	for flag, i in visible_flags {
+		if i == divider_index {
+			SPACING :: 2 // Number of spaces before the '|' from below.
+			strings.write_byte(&builder, '\t')
+			spacing := strings.repeat(" ", SPACING + longest_flag_length, context.temp_allocator)
+			strings.write_string(&builder, spacing)
+			strings.write_string(&builder, "|\n")
+		}
+
+		strings.write_byte(&builder, '\t')
+
+		if flag.name == INTERNAL_VARIADIC_FLAG {
+			strings.write_string(&builder, flag.type_description)
+		} else {
+			strings.write_string(&builder, flag_prefix)
+			strings.write_string(&builder, flag.name)
+			if !flag.is_boolean {
+				strings.write_string(&builder, flag_assignment)
+			}
+			strings.write_string(&builder, flag.type_description)
+		}
+
+		if strings.contains_rune(flag.usage, '\n') {
+			// Multi-line usage documentation. Let's make it look nice.
+			usage_builder := strings.builder_make(context.temp_allocator)
+
+			strings.write_byte(&usage_builder, '\n')
+			iter := strings.trim_space(flag.usage)
+			for line in strings.split_lines_iterator(&iter) {
+				strings.write_string(&usage_builder, "\t\t")
+				strings.write_string(&usage_builder, strings.trim_left_space(line))
+				strings.write_byte(&usage_builder, '\n')
+			}
+
+			strings.write_string(&builder, strings.to_string(usage_builder))
+		} else {
+			// Single-line usage documentation.
+			spacing := strings.repeat(" ",
+				(longest_flag_length) - flag.full_length,
+				context.temp_allocator)
+
+			strings.write_string(&builder, spacing)
+			strings.write_string(&builder, "  | ")
+			strings.write_string(&builder, flag.usage)
+			strings.write_byte(&builder, '\n')
+		}
+	}
+
+	fmt.wprint(out, strings.to_string(builder))
+}

+ 130 - 0
core/flags/util.odin

@@ -0,0 +1,130 @@
+package flags
+
+import "core:fmt"
+@require import "core:os"
+@require import "core:path/filepath"
+import "core:strings"
+
+/*
+Parse any arguments into an annotated struct or exit if there was an error.
+
+*Allocates Using Provided Allocator*
+
+This is a convenience wrapper over `parse` and `print_errors`.
+
+Inputs:
+- model: A pointer to an annotated struct.
+- program_args: A slice of strings, usually `os.args`.
+- style: The argument parsing style.
+- allocator: (default: context.allocator)
+- loc: The caller location for debugging purposes (default: #caller_location)
+*/
+@(optimization_mode="size")
+parse_or_exit :: proc(
+	model: ^$T,
+	program_args: []string,
+	style: Parsing_Style = .Odin,
+	allocator := context.allocator,
+	loc := #caller_location,
+) {
+	assert(len(program_args) > 0, "Program arguments slice is empty.", loc)
+
+	program := filepath.base(program_args[0])
+	args: []string
+
+	if len(program_args) > 1 {
+		args = program_args[1:]
+	}
+
+	error := parse(model, args, style)
+	if error != nil {
+		stderr := os.stream_from_handle(os.stderr)
+
+		if len(args) == 0 {
+			// No arguments entered, and there was an error; show the usage,
+			// specifically on STDERR.
+			write_usage(stderr, T, program, style)
+			fmt.wprintln(stderr)
+		}
+
+		print_errors(T, error, program, style)
+
+		_, was_help_request := error.(Help_Request)
+		os.exit(0 if was_help_request else 1)
+	}
+}
+/*
+Print out any errors that may have resulted from parsing.
+
+All error messages print to STDERR, while usage goes to STDOUT, if requested.
+
+Inputs:
+- data_type: The typeid of the data structure to describe, if usage is requested.
+- error: The error returned from `parse`.
+- style: The argument parsing style, required to show flags in the proper style, when usage is shown.
+*/
+@(optimization_mode="size")
+print_errors :: proc(data_type: typeid, error: Error, program: string, style: Parsing_Style = .Odin) {
+	stderr := os.stream_from_handle(os.stderr)
+	stdout := os.stream_from_handle(os.stdout)
+
+	switch specific_error in error {
+	case Parse_Error:
+		fmt.wprintfln(stderr, "[%T.%v] %s", specific_error, specific_error.reason, specific_error.message)
+	case Open_File_Error:
+		fmt.wprintfln(stderr, "[%T#%i] Unable to open file with perms 0o%o in mode 0x%x: %s",
+			specific_error,
+			specific_error.errno,
+			specific_error.perms,
+			specific_error.mode,
+			specific_error.filename)
+	case Validation_Error:
+		fmt.wprintfln(stderr, "[%T] %s", specific_error, specific_error.message)
+	case Help_Request:
+		write_usage(stdout, data_type, program, style)
+	}
+}
+/*
+Get the value for a subtag.
+
+This is useful if you need to parse through the `args` tag for a struct field
+on a custom type setter or custom flag checker.
+
+Example:
+
+	import "core:flags"
+	import "core:fmt"
+
+	subtag_example :: proc() {
+		args_tag := "precision=3,signed"
+
+		precision, has_precision := flags.get_subtag(args_tag, "precision")
+		signed, is_signed := flags.get_subtag(args_tag, "signed")
+
+		fmt.printfln("precision = %q, %t", precision, has_precision)
+		fmt.printfln("signed = %q, %t", signed, is_signed)
+	}
+
+Output:
+
+	precision = "3", true
+	signed = "", true
+
+*/
+get_subtag :: proc(tag, id: string) -> (value: string, ok: bool) {
+	// This proc was initially private in `internal_rtti.odin`, but given how
+	// useful it would be to custom type setters and flag checkers, it lives
+	// here now.
+
+	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
+}

+ 37 - 0
core/flags/validation.odin

@@ -0,0 +1,37 @@
+package flags
+
+/*
+Check a flag after parsing, during the validation stage.
+
+Inputs:
+- model: A raw pointer to the data structure provided to `parse`.
+- name: The name of the flag being checked.
+- value: An `any` type that contains the value to be checked.
+- args_tag: The `args` tag from within the struct.
+
+Returns:
+- error: An error message, or an empty string if no error occurred.
+*/
+Custom_Flag_Checker :: #type proc(
+	model:    rawptr,
+	name:     string,
+	value:    any,
+	args_tag: string,
+) -> (
+	error: string,
+)
+
+@(private)
+global_custom_flag_checker: Custom_Flag_Checker
+
+/*
+Set the global custom flag checker.
+
+Note that only one can be active at a time.
+
+Inputs:
+- checker: The flag checker. Pass `nil` to disable any previously set checker.
+*/
+register_flag_checker :: proc(checker: Custom_Flag_Checker) {
+	global_custom_flag_checker = checker
+}

+ 1350 - 0
tests/core/flags/test_core_flags.odin

@@ -0,0 +1,1350 @@
+package test_core_flags
+
+import "base:runtime"
+import "core:bytes"
+import "core:flags"
+import "core:fmt"
+@require import "core:log"
+import "core:math"
+@require import "core:net"
+import "core:os"
+import "core:strings"
+import "core:testing"
+import "core:time/datetime"
+
+@(test)
+test_no_args :: proc(t: ^testing.T) {
+	S :: struct {
+		a: string,
+	}
+	s: S
+	args: []string
+	result := flags.parse(&s, args)
+	testing.expect_value(t, result, nil)
+}
+
+@(test)
+test_two_flags :: proc(t: ^testing.T) {
+	S :: struct {
+		i: string,
+		o: string,
+	}
+	s: S
+	args := [?]string { "-i:hellope", "-o:world" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.i, "hellope")
+	testing.expect_value(t, s.o, "world")
+}
+
+@(test)
+test_extra_arg :: proc(t: ^testing.T) {
+	S :: struct {
+		a: string,
+	}
+	s: S
+	args := [?]string { "-a:hellope", "world" }
+	result := flags.parse(&s, args[:])
+	err, ok := result.(flags.Parse_Error)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+	if ok {
+		testing.expect_value(t, err.reason, flags.Parse_Error_Reason.Extra_Positional)
+	}
+}
+
+@(test)
+test_assignment_oddities :: proc(t: ^testing.T) {
+	S :: struct {
+		s: string,
+	}
+	s: S
+
+	{
+		args := [?]string { "-s:=" }
+		result := flags.parse(&s, args[:])
+		testing.expect_value(t, result, nil)
+		testing.expect_value(t, s.s, "=")
+	}
+
+	{
+		args := [?]string { "-s=:" }
+		result := flags.parse(&s, args[:])
+		testing.expect_value(t, result, nil)
+		testing.expect_value(t, s.s, ":")
+	}
+
+	{
+		args := [?]string { "-" }
+		result := flags.parse(&s, args[:])
+		err, ok := result.(flags.Parse_Error)
+		testing.expectf(t, ok, "unexpected result: %v", result)
+		if ok {
+			testing.expect_value(t, err.reason, flags.Parse_Error_Reason.No_Flag)
+		}
+	}
+}
+
+@(test)
+test_string_into_int :: proc(t: ^testing.T) {
+	S :: struct {
+		n: int,
+	}
+	s: S
+	args := [?]string { "-n:hellope" }
+	result := flags.parse(&s, args[:])
+	err, ok := result.(flags.Parse_Error)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+	if ok {
+		testing.expect_value(t, err.reason, flags.Parse_Error_Reason.Bad_Value)
+	}
+}
+
+@(test)
+test_string_into_bool :: proc(t: ^testing.T) {
+	S :: struct {
+		b: bool,
+	}
+	s: S
+	args := [?]string { "-b:hellope" }
+	result := flags.parse(&s, args[:])
+	err, ok := result.(flags.Parse_Error)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+	if ok {
+		testing.expect_value(t, err.reason, flags.Parse_Error_Reason.Bad_Value)
+	}
+}
+
+@(test)
+test_all_bools :: proc(t: ^testing.T) {
+	S :: struct {
+		a: bool,
+		b: b8,
+		c: b16,
+		d: b32,
+		e: b64,
+	}
+	s: S
+	s.a = true
+	s.c = true
+	args := [?]string { "-a:false", "-b:true", "-c:0", "-d", "-e:1" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a, false)
+	testing.expect_value(t, s.b, true)
+	testing.expect_value(t, s.c, false)
+	testing.expect_value(t, s.d, true)
+	testing.expect_value(t, s.e, true)
+}
+
+@(test)
+test_all_ints :: proc(t: ^testing.T) {
+	S :: struct {
+		a: u8,
+		b: i8,
+		c: u16,
+		d: i16,
+		e: u32,
+		f: i32,
+		g: u64,
+		i: i64,
+		j: u128,
+		k: i128,
+	}
+
+	s: S
+	args := [?]string {
+		fmt.tprintf("-a:%i", max(u8)),
+		fmt.tprintf("-b:%i", min(i8)),
+		fmt.tprintf("-c:%i", max(u16)),
+		fmt.tprintf("-d:%i", min(i16)),
+		fmt.tprintf("-e:%i", max(u32)),
+		fmt.tprintf("-f:%i", min(i32)),
+		fmt.tprintf("-g:%i", max(u64)),
+		fmt.tprintf("-i:%i", min(i64)),
+		fmt.tprintf("-j:%i", max(u128)),
+		fmt.tprintf("-k:%i", min(i128)),
+	}
+
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a, max(u8))
+	testing.expect_value(t, s.b, min(i8))
+	testing.expect_value(t, s.c, max(u16))
+	testing.expect_value(t, s.d, min(i16))
+	testing.expect_value(t, s.e, max(u32))
+	testing.expect_value(t, s.f, min(i32))
+	testing.expect_value(t, s.g, max(u64))
+	testing.expect_value(t, s.i, min(i64))
+	testing.expect_value(t, s.j, max(u128))
+	testing.expect_value(t, s.k, min(i128))
+}
+
+@(test)
+test_all_floats :: proc(t: ^testing.T) {
+	S :: struct {
+		a: f16,
+		b: f32,
+		c: f64,
+		d: f64,
+		e: f64,
+	}
+	s: S
+	args := [?]string { "-a:100", "-b:3.14", "-c:-123.456", "-d:nan", "-e:inf" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a, 100)
+	testing.expect_value(t, s.b, 3.14)
+	testing.expect_value(t, s.c, -123.456)
+	testing.expectf(t, math.is_nan(s.d), "expected NaN, got %v", s.d)
+	testing.expectf(t, math.is_inf(s.e, +1), "expected +Inf, got %v", s.e)
+}
+
+@(test)
+test_all_enums :: proc(t: ^testing.T) {
+	E :: enum { A, B }
+	S :: struct {
+		nameless: enum { C, D },
+		named: E,
+	}
+	s: S
+	args := [?]string { "-nameless:D", "-named:B" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, cast(int)s.nameless, 1)
+	testing.expect_value(t, s.named, E.B)
+}
+
+@(test)
+test_all_complex :: proc(t: ^testing.T) {
+	S :: struct {
+		a: complex32,
+		b: complex64,
+		c: complex128,
+	}
+	s: S
+	args := [?]string { "-a:1+0i", "-b:3+7i", "-c:NaNNaNi" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, real(s.a), 1)
+	testing.expect_value(t, imag(s.a), 0)
+	testing.expect_value(t, real(s.b), 3)
+	testing.expect_value(t, imag(s.b), 7)
+	testing.expectf(t, math.is_nan(real(s.c)), "expected NaN, got %v", real(s.c))
+	testing.expectf(t, math.is_nan(imag(s.c)), "expected NaN, got %v", imag(s.c))
+}
+
+@(test)
+test_all_quaternion :: proc(t: ^testing.T) {
+	S :: struct {
+		a: quaternion64,
+		b: quaternion128,
+		c: quaternion256,
+	}
+	s: S
+	args := [?]string { "-a:1+0i+1j+0k", "-b:3+7i+5j-3k", "-c:NaNNaNi+Infj-Infk" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+
+	raw_a := (cast(^runtime.Raw_Quaternion64)&s.a)
+	raw_b := (cast(^runtime.Raw_Quaternion128)&s.b)
+	raw_c := (cast(^runtime.Raw_Quaternion256)&s.c)
+
+	testing.expect_value(t, raw_a.real, 1)
+	testing.expect_value(t, raw_a.imag, 0)
+	testing.expect_value(t, raw_a.jmag, 1)
+	testing.expect_value(t, raw_a.kmag, 0)
+
+	testing.expect_value(t, raw_b.real, 3)
+	testing.expect_value(t, raw_b.imag, 7)
+	testing.expect_value(t, raw_b.jmag, 5)
+	testing.expect_value(t, raw_b.kmag, -3)
+
+	testing.expectf(t, math.is_nan(raw_c.real), "expected NaN, got %v", raw_c.real)
+	testing.expectf(t, math.is_nan(raw_c.imag), "expected NaN, got %v", raw_c.imag)
+	testing.expectf(t, math.is_inf(raw_c.jmag, +1), "expected +Inf, got %v", raw_c.jmag)
+	testing.expectf(t, math.is_inf(raw_c.kmag, -1), "expected -Inf, got %v", raw_c.kmag)
+}
+
+@(test)
+test_all_bit_sets :: proc(t: ^testing.T) {
+	E :: enum {
+		Option_A,
+		Option_B,
+	}
+	S :: struct {
+		a: bit_set[0..<8],
+		b: bit_set[0..<16; u16],
+		c: bit_set[16..<18; rune],
+		d: bit_set[0..<1; i8],
+		e: bit_set[0..<128],
+		f: bit_set[-32..<32],
+		g: bit_set[E],
+		i: bit_set[E; u8],
+	}
+	s: S
+	{
+		args := [?]string {
+			"-a:10101",
+			"-b:0000_0000_0000_0001",
+			"-c:11",
+			"-d:___1",
+			"-e:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
+			"-f:1",
+			"-g:01",
+			"-i:1",
+		}
+		result := flags.parse(&s, args[:])
+		testing.expect_value(t, result, nil)
+		testing.expect_value(t, s.a, bit_set[0..<8]{0, 2, 4})
+		testing.expect_value(t, s.b, bit_set[0..<16; u16]{15})
+		testing.expect_value(t, s.c, bit_set[16..<18; rune]{16, 17})
+		testing.expect_value(t, s.d, bit_set[0..<1; i8]{0})
+		testing.expect_value(t, s.e, bit_set[0..<128]{127})
+		testing.expect_value(t, s.f, bit_set[-32..<32]{-32})
+		testing.expect_value(t, s.g, bit_set[E]{E.Option_B})
+		testing.expect_value(t, s.i, bit_set[E; u8]{E.Option_A})
+	}
+	{
+		args := [?]string { "-d:11" }
+		result := flags.parse(&s, args[:])
+		err, ok := result.(flags.Parse_Error)
+		testing.expectf(t, ok, "unexpected result: %v", result)
+		if ok {
+			testing.expect_value(t, err.reason, flags.Parse_Error_Reason.Bad_Value)
+		}
+	}
+}
+
+@(test)
+test_all_strings :: proc(t: ^testing.T) {
+	S :: struct {
+		a, b, c: string,
+		d: cstring,
+	}
+	s: S
+	args := [?]string { "-a:hi", "-b:hellope", "-c:spaced out", "-d:cstr", "-d:cstr-overwrite" }
+	result := flags.parse(&s, args[:])
+	defer delete(s.d)
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a, "hi")
+	testing.expect_value(t, s.b, "hellope")
+	testing.expect_value(t, s.c, "spaced out")
+	testing.expect_value(t, s.d, "cstr-overwrite")
+}
+
+@(test)
+test_runes :: proc(t: ^testing.T) {
+	S :: struct {
+		a, b, c: rune,
+	}
+	s: S
+	args := [?]string { "-a:a", "-b:ツ", "-c:\U0010FFFF" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a, 'a')
+	testing.expect_value(t, s.b, 'ツ')
+	testing.expect_value(t, s.c, '\U0010FFFF')
+}
+
+@(test)
+test_no_value :: proc(t: ^testing.T) {
+	S :: struct {
+		a: rune,
+	}
+	s: S
+
+	{
+		args := [?]string { "-a:" }
+		result := flags.parse(&s, args[:])
+		err, ok := result.(flags.Parse_Error)
+		testing.expectf(t, ok, "unexpected result: %v", result)
+		if ok {
+			testing.expect_value(t, err.reason, flags.Parse_Error_Reason.No_Value)
+		}
+	}
+
+	{
+		args := [?]string { "-a=" }
+		result := flags.parse(&s, args[:])
+		err, ok := result.(flags.Parse_Error)
+		testing.expectf(t, ok, "unexpected result: %v", result)
+		if ok {
+			testing.expect_value(t, err.reason, flags.Parse_Error_Reason.No_Value)
+		}
+	}
+}
+
+@(test)
+test_overflow :: proc(t: ^testing.T) {
+	S :: struct {
+		a: u8,
+	}
+	s: S
+	args := [?]string { "-a:256" }
+	result := flags.parse(&s, args[:])
+	err, ok := result.(flags.Parse_Error)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+	if ok {
+		testing.expect_value(t, err.reason, flags.Parse_Error_Reason.Bad_Value)
+	}
+}
+
+@(test)
+test_underflow :: proc(t: ^testing.T) {
+	S :: struct {
+		a: i8,
+	}
+	s: S
+	args := [?]string { "-a:-129" }
+	result := flags.parse(&s, args[:])
+	err, ok := result.(flags.Parse_Error)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+	if ok {
+		testing.expect_value(t, err.reason, flags.Parse_Error_Reason.Bad_Value)
+	}
+}
+
+@(test)
+test_arrays :: proc(t: ^testing.T) {
+	S :: struct {
+		a: [dynamic]string,
+		b: [dynamic]int,
+	}
+	s: S
+	args := [?]string { "-a:abc", "-b:1", "-a:foo", "-b:3" }
+	result := flags.parse(&s, args[:])
+	defer {
+		delete(s.a)
+		delete(s.b)
+	}
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, len(s.a), 2)
+	testing.expect_value(t, len(s.b), 2)
+
+	if len(s.a) < 2 || len(s.b) < 2 {
+		return
+	}
+
+	testing.expect_value(t, s.a[0], "abc")
+	testing.expect_value(t, s.a[1], "foo")
+	testing.expect_value(t, s.b[0], 1)
+	testing.expect_value(t, s.b[1], 3)
+}
+
+@(test)
+test_varargs :: proc(t: ^testing.T) {
+	S :: struct {
+		varg: [dynamic]string,
+	}
+	s: S
+	args := [?]string { "abc", "foo", "bar" }
+	result := flags.parse(&s, args[:])
+	defer delete(s.varg)
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, len(s.varg), 3)
+
+	if len(s.varg) < 3 {
+		return
+	}
+
+	testing.expect_value(t, s.varg[0], "abc")
+	testing.expect_value(t, s.varg[1], "foo")
+	testing.expect_value(t, s.varg[2], "bar")
+}
+
+@(test)
+test_mixed_varargs :: proc(t: ^testing.T) {
+	S :: struct {
+		input: string `args:"pos=0"`,
+		varg: [dynamic]string,
+	}
+	s: S
+	args := [?]string { "abc", "foo", "bar" }
+	result := flags.parse(&s, args[:])
+	defer delete(s.varg)
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, len(s.varg), 2)
+
+	if len(s.varg) < 2 {
+		return
+	}
+
+	testing.expect_value(t, s.input, "abc")
+	testing.expect_value(t, s.varg[0], "foo")
+	testing.expect_value(t, s.varg[1], "bar")
+}
+
+@(test)
+test_maps :: proc(t: ^testing.T) {
+	S :: struct {
+		a: map[string]string,
+		b: map[string]int,
+	}
+	s: S
+	args := [?]string { "-a:abc=foo", "-b:bar=42" }
+	result := flags.parse(&s, args[:])
+	defer {
+		delete(s.a)
+		delete(s.b)
+	}
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, len(s.a), 1)
+	testing.expect_value(t, len(s.b), 1)
+
+	if len(s.a) < 1 || len(s.b) < 1 {
+		return
+	}
+
+	abc, has_abc := s.a["abc"]
+	bar, has_bar := s.b["bar"]
+
+	testing.expect(t, has_abc, "expected map to have `abc` key set")
+	testing.expect(t, has_bar, "expected map to have `bar` key set")
+	testing.expect_value(t, abc, "foo")
+	testing.expect_value(t, bar, 42)
+}
+
+@(test)
+test_invalid_map_syntax :: proc(t: ^testing.T) {
+	S :: struct {
+		a: map[string]string,
+	}
+	s: S
+	args := [?]string { "-a:foo:42" }
+	result := flags.parse(&s, args[:])
+	err, ok := result.(flags.Parse_Error)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+	if ok {
+		testing.expect_value(t, err.reason, flags.Parse_Error_Reason.No_Value)
+	}
+}
+
+@(test)
+test_underline_name_to_dash :: proc(t: ^testing.T) {
+	S :: struct {
+		a_b: int,
+	}
+	s: S
+	args := [?]string { "-a-b:3" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a_b, 3)
+}
+
+@(test)
+test_tags_pos :: proc(t: ^testing.T) {
+	S :: struct {
+		b: int `args:"pos=1"`,
+		a: int `args:"pos=0"`,
+	}
+	s: S
+	args := [?]string { "42", "99" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a, 42)
+	testing.expect_value(t, s.b, 99)
+}
+
+@(test)
+test_tags_name :: proc(t: ^testing.T) {
+	S :: struct {
+		a: int `args:"name=alice"`,
+		b: int `args:"name=bill"`,
+	}
+	s: S
+	args := [?]string { "-alice:1", "-bill:2" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a, 1)
+	testing.expect_value(t, s.b, 2)
+}
+
+@(test)
+test_tags_required :: proc(t: ^testing.T) {
+	S :: struct {
+		a: int,
+		b: int `args:"required"`,
+	}
+	s: S
+	args := [?]string { "-a:1" }
+	result := flags.parse(&s, args[:])
+	_, ok := result.(flags.Validation_Error)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+}
+
+@(test)
+test_tags_required_pos :: proc(t: ^testing.T) {
+	S :: struct {
+		a: int `args:"pos=0,required"`,
+		b: int `args:"pos=1"`,
+	}
+	s: S
+	args := [?]string { "-b:5" }
+	result := flags.parse(&s, args[:])
+	_, ok := result.(flags.Validation_Error)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+}
+
+@(test)
+test_tags_required_limit_min :: proc(t: ^testing.T) {
+	S :: struct {
+		n: [dynamic]int `args:"required=3"`,
+	}
+
+	{
+		s: S
+		args := [?]string { "-n:1" }
+		result := flags.parse(&s, args[:])
+		defer delete(s.n)
+		_, ok := result.(flags.Validation_Error)
+		testing.expectf(t, ok, "unexpected result: %v", result)
+	}
+
+	{
+		s: S
+		args := [?]string { "-n:3", "-n:5", "-n:7" }
+		result := flags.parse(&s, args[:])
+		defer delete(s.n)
+		testing.expect_value(t, result, nil)
+		testing.expect_value(t, len(s.n), 3)
+
+		if len(s.n) == 3 {
+			testing.expect_value(t, s.n[0], 3)
+			testing.expect_value(t, s.n[1], 5)
+			testing.expect_value(t, s.n[2], 7)
+		}
+	}
+}
+
+@(test)
+test_tags_required_limit_min_max :: proc(t: ^testing.T) {
+	S :: struct {
+		n: [dynamic]int `args:"required=2<4"`,
+	}
+
+	{
+		s: S
+		args := [?]string { "-n:1" }
+		result := flags.parse(&s, args[:])
+		defer delete(s.n)
+		_, ok := result.(flags.Validation_Error)
+		testing.expectf(t, ok, "unexpected result: %v", result)
+	}
+
+	{
+		s: S
+		args := [?]string { "-n:1", "-n:2", "-n:3", "-n:4" }
+		result := flags.parse(&s, args[:])
+		defer delete(s.n)
+		_, ok := result.(flags.Validation_Error)
+		testing.expectf(t, ok, "unexpected result: %v", result)
+	}
+
+	{
+		s: S
+		args := [?]string { "-n:3", "-n:5", "-n:7" }
+		result := flags.parse(&s, args[:])
+		defer delete(s.n)
+		testing.expect_value(t, result, nil)
+		testing.expect_value(t, len(s.n), 3)
+
+		if len(s.n) == 3 {
+			testing.expect_value(t, s.n[0], 3)
+			testing.expect_value(t, s.n[1], 5)
+			testing.expect_value(t, s.n[2], 7)
+		}
+	}
+}
+
+@(test)
+test_tags_required_limit_max :: proc(t: ^testing.T) {
+	S :: struct {
+		n: [dynamic]int `args:"required=<4"`,
+	}
+
+	{
+		s: S
+		args: []string
+		result := flags.parse(&s, args)
+		testing.expect_value(t, result, nil)
+	}
+
+	{
+		s: S
+		args := [?]string { "-n:1", "-n:2", "-n:3", "-n:4" }
+		result := flags.parse(&s, args[:])
+		defer delete(s.n)
+		_, ok := result.(flags.Validation_Error)
+		testing.expectf(t, ok, "unexpected result: %v", result)
+	}
+
+	{
+		s: S
+		args := [?]string { "-n:3", "-n:5", "-n:7" }
+		result := flags.parse(&s, args[:])
+		defer delete(s.n)
+		testing.expect_value(t, result, nil)
+		testing.expect_value(t, len(s.n), 3)
+
+		if len(s.n) == 3 {
+			testing.expect_value(t, s.n[0], 3)
+			testing.expect_value(t, s.n[1], 5)
+			testing.expect_value(t, s.n[2], 7)
+		}
+	}
+}
+
+@(test)
+test_tags_pos_out_of_order :: proc(t: ^testing.T) {
+	S :: struct {
+		a: int `args:"pos=2"`,
+		varg: [dynamic]int,
+	}
+	s: S
+	args := [?]string { "1", "2", "3", "4" }
+	result := flags.parse(&s, args[:])
+	defer delete(s.varg)
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, len(s.varg), 3)
+
+	if len(s.varg) < 3 {
+		return
+	}
+
+	testing.expect_value(t, s.a, 3)
+	testing.expect_value(t, s.varg[0], 1)
+	testing.expect_value(t, s.varg[1], 2)
+	testing.expect_value(t, s.varg[2], 4)
+}
+
+@(test)
+test_missing_flag :: proc(t: ^testing.T) {
+	S :: struct {
+		a: int,
+	}
+	s: S
+	args := [?]string { "-b" }
+	result := flags.parse(&s, args[:])
+	err, ok := result.(flags.Parse_Error)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+	if ok {
+		testing.expect_value(t, err.reason, flags.Parse_Error_Reason.Missing_Flag)
+	}
+}
+
+@(test)
+test_alt_syntax :: proc(t: ^testing.T) {
+	S :: struct {
+		a: int,
+	}
+	s: S
+	args := [?]string { "-a=3" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a, 3)
+}
+
+@(test)
+test_strict_returns_first_error :: proc(t: ^testing.T) {
+	S :: struct {
+		b: int,
+		c: int,
+	}
+	s: S
+	args := [?]string { "-a=3", "-b=3" }
+	result := flags.parse(&s, args[:], strict=true)
+	err, ok := result.(flags.Parse_Error)
+	testing.expect_value(t, s.b, 0)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+	if ok {
+		testing.expect_value(t, err.reason, flags.Parse_Error_Reason.Missing_Flag)
+	}
+}
+
+@(test)
+test_non_strict_returns_last_error :: proc(t: ^testing.T) {
+	S :: struct {
+		a: int,
+		b: int,
+	}
+	s: S
+	args := [?]string { "-a=foo", "-b=2", "-c=3" }
+	result := flags.parse(&s, args[:], strict=false)
+	err, ok := result.(flags.Parse_Error)
+	testing.expect_value(t, s.b, 2)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+	if ok {
+		testing.expect_value(t, err.reason, flags.Parse_Error_Reason.Missing_Flag)
+	}
+}
+
+@(test)
+test_map_overwrite :: proc(t: ^testing.T) {
+	S :: struct {
+		m: map[string]int,
+	}
+	s: S
+	args := [?]string { "-m:foo=3", "-m:foo=5" }
+	result := flags.parse(&s, args[:])
+	defer delete(s.m)
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, len(s.m), 1)
+	foo, has_foo := s.m["foo"]
+	testing.expect(t, has_foo, "expected map to have `foo` key set")
+	testing.expect_value(t, foo, 5)
+}
+
+@(test)
+test_maps_of_arrays :: proc(t: ^testing.T) {
+	// Why you would ever want to do this, I don't know, but it's possible!
+	S :: struct {
+		m: map[string][dynamic]int,
+	}
+	s: S
+	args := [?]string { "-m:foo=1", "-m:foo=2", "-m:bar=3" }
+	result := flags.parse(&s, args[:])
+	defer {
+		for _, v in s.m {
+			delete(v)
+		}
+		delete(s.m)
+	}
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, len(s.m), 2)
+
+	if len(s.m) != 2 {
+		return
+	}
+
+	foo, has_foo := s.m["foo"]
+	bar, has_bar := s.m["bar"]
+
+	testing.expect_value(t, has_foo, true)
+	testing.expect_value(t, has_bar, true)
+
+	if has_foo {
+		testing.expect_value(t, len(foo), 2)
+		if len(foo) == 2 {
+			testing.expect_value(t, foo[0], 1)
+			testing.expect_value(t, foo[1], 2)
+		}
+	}
+
+	if has_bar {
+		testing.expect_value(t, len(bar), 1)
+		if len(bar) == 1 {
+			testing.expect_value(t, bar[0], 3)
+		}
+	}
+}
+
+@(test)
+test_builtin_help_flag :: proc(t: ^testing.T) {
+	S :: struct {}
+	s: S
+
+	args_short  := [?]string { "-h" }
+	args_normal := [?]string { "-help" }
+
+	result := flags.parse(&s, args_short[:])
+	_, ok := result.(flags.Help_Request)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+
+	result = flags.parse(&s, args_normal[:])
+	_, ok = result.(flags.Help_Request)
+	testing.expectf(t, ok, "unexpected result: %v", result)
+}
+
+// This test makes sure that if a positional argument is specified, it won't be
+// overwritten by an unspecified positional, which should follow the principle
+// of least surprise for the user.
+@(test)
+test_pos_nonoverlap :: proc(t: ^testing.T) {
+	S :: struct {
+		a: int `args:"pos=0"`,
+		b: int `args:"pos=1"`,
+	}
+	s: S
+
+	args := [?]string { "-a:3", "5" }
+
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a, 3)
+	testing.expect_value(t, s.b, 5)
+}
+
+// This test ensures the underlying `bit_array` container handles many
+// arguments in a sane manner.
+@(test)
+test_pos_many_args :: proc(t: ^testing.T) {
+	S :: struct {
+		varg: [dynamic]int,
+		a: int `args:"pos=0,required"`,
+		b: int `args:"pos=64,required"`,
+		c: int `args:"pos=66,required"`,
+		d: int `args:"pos=129,required"`,
+	}
+	s: S
+
+	args: [dynamic]string
+	defer delete(s.varg)
+
+	for i in 0 ..< 130 { append(&args, fmt.aprintf("%i", 1 + i)) }
+	defer {
+		for a in args {
+			delete(a)
+		}
+		delete(args)
+	}
+
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+
+	testing.expect_value(t, s.a, 1)
+	for i in 1 ..< 63 { testing.expect_value(t, s.varg[i], 2 + i) }
+	testing.expect_value(t, s.b, 65)
+	testing.expect_value(t, s.varg[63], 66)
+	testing.expect_value(t, s.c, 67)
+	testing.expect_value(t, s.varg[64], 68)
+	testing.expect_value(t, s.varg[65], 69)
+	testing.expect_value(t, s.varg[66], 70)
+	for i in 67 ..< 126 { testing.expect_value(t, s.varg[i], 4 + i) }
+	testing.expect_value(t, s.d, 130)
+}
+
+@(test)
+test_unix :: proc(t: ^testing.T) {
+	S :: struct {
+		a: string,
+	}
+	s: S
+
+	{
+		args := [?]string { "--a", "hellope" }
+
+		result := flags.parse(&s, args[:], .Unix)
+		testing.expect_value(t, result, nil)
+		testing.expect_value(t, s.a, "hellope")
+	}
+
+	{
+		args := [?]string { "-a", "hellope", "--a", "world" }
+
+		result := flags.parse(&s, args[:], .Unix)
+		testing.expect_value(t, result, nil)
+		testing.expect_value(t, s.a, "world")
+	}
+
+	{
+		args := [?]string { "-a=hellope" }
+
+		result := flags.parse(&s, args[:], .Unix)
+		testing.expect_value(t, result, nil)
+		testing.expect_value(t, s.a, "hellope")
+	}
+}
+
+@(test)
+test_unix_variadic :: proc(t: ^testing.T) {
+	S :: struct {
+		a: [dynamic]int `args:"variadic"`,
+	}
+	s: S
+
+	args := [?]string { "--a", "7", "32", "11" }
+
+	result := flags.parse(&s, args[:], .Unix)
+	defer delete(s.a)
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, len(s.a), 3)
+
+	if len(s.a) < 3 {
+		return
+	}
+
+	testing.expect_value(t, s.a[0], 7)
+	testing.expect_value(t, s.a[1], 32)
+	testing.expect_value(t, s.a[2], 11)
+}
+
+@(test)
+test_unix_variadic_limited :: proc(t: ^testing.T) {
+	S :: struct {
+		a: [dynamic]int `args:"variadic=2"`,
+		b: int,
+	}
+	s: S
+
+	args := [?]string { "-a", "11", "101", "-b", "3" }
+
+	result := flags.parse(&s, args[:], .Unix)
+	defer delete(s.a)
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, len(s.a), 2)
+
+	if len(s.a) < 2 {
+		return
+	}
+
+	testing.expect_value(t, s.a[0], 11)
+	testing.expect_value(t, s.a[1], 101)
+	testing.expect_value(t, s.b, 3)
+}
+
+@(test)
+test_unix_positional :: proc(t: ^testing.T) {
+	S :: struct {
+		a: int `args:"pos=1"`,
+		b: int `args:"pos=0"`,
+	}
+	s: S
+
+	args := [?]string { "-b", "17", "11" }
+
+	result := flags.parse(&s, args[:], .Unix)
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a, 11)
+	testing.expect_value(t, s.b, 17)
+}
+
+@(test)
+test_unix_positional_with_variadic :: proc(t: ^testing.T) {
+	S :: struct {
+		varg: [dynamic]int,
+		v: [dynamic]int `args:"variadic"`,
+	}
+	s: S
+
+	args := [?]string { "35", "-v", "17", "11" }
+
+	result := flags.parse(&s, args[:], .Unix)
+	defer {
+		delete(s.varg)
+		delete(s.v)
+	}
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, len(s.varg), 1)
+	testing.expect_value(t, len(s.v), 2)
+}
+
+// This test ensures there are no bad frees with cstrings.
+@(test)
+test_if_dynamic_cstrings_get_freed :: proc(t: ^testing.T) {
+	S :: struct {
+		varg: [dynamic]cstring,
+	}
+	s: S
+
+	args := [?]string { "Hellope", "world!" }
+	result := flags.parse(&s, args[:])
+	defer {
+		for v in s.varg {
+			delete(v)
+		}
+		delete(s.varg)
+	}
+	testing.expect_value(t, result, nil)
+}
+
+// This test ensures there are no double allocations with cstrings.
+@(test)
+test_if_map_cstrings_get_freed :: proc(t: ^testing.T) {
+	S :: struct {
+		m: map[cstring]cstring,
+	}
+	s: S
+
+	args := [?]string { "-m:hellope=world", "-m:hellope=bar", "-m:hellope=foo" }
+	result := flags.parse(&s, args[:])
+	defer {
+		for _, v in s.m {
+			delete(v)
+		}
+		delete(s.m)
+	}
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.m["hellope"], "foo")
+}
+
+@(test)
+test_os_handle :: proc(t: ^testing.T) {
+	TEMPORARY_FILENAME :: "test_core_flags_write_test_output_data"
+
+	test_data := "Hellope!"
+
+	W :: struct {
+		outf: os.Handle `args:"file=cw"`,
+	}
+	w: W
+
+	args := [?]string { fmt.tprintf("-outf:%s", TEMPORARY_FILENAME) }
+	result := flags.parse(&w, args[:])
+	testing.expect_value(t, result, nil)
+	if result != nil {
+		return
+	}
+	defer os.close(w.outf)
+	os.write_string(w.outf, test_data)
+
+	R :: struct {
+		inf: os.Handle `args:"file=r"`,
+	}
+	r: R
+
+	args = [?]string { fmt.tprintf("-inf:%s", TEMPORARY_FILENAME) }
+	result = flags.parse(&r, args[:])
+	testing.expect_value(t, result, nil)
+	if result != nil {
+		return
+	}
+	defer os.close(r.inf)
+	data, read_ok := os.read_entire_file_from_handle(r.inf, context.temp_allocator)
+	testing.expect_value(t, read_ok, true)
+	file_contents_equal := 0 == bytes.compare(transmute([]u8)test_data, data)
+	testing.expectf(t, file_contents_equal, "expected file contents to be the same, got %v", data)
+
+	if file_contents_equal {
+		// Delete the file now that we're done.
+		//
+		// This is not done as a defer or all the time, just in case the file
+		// is useful to debugging.
+		testing.expect_value(t, os.remove(TEMPORARY_FILENAME), os.ERROR_NONE)
+	}
+}
+
+@(test)
+test_distinct_types :: proc(t: ^testing.T) {
+	I :: distinct int
+	S :: struct {
+		base_i: I `args:"indistinct"`,
+		unmodified_i: I,
+	}
+	s: S
+
+	{
+		args := [?]string {"-base-i:1"}
+		result := flags.parse(&s, args[:])
+		testing.expect_value(t, result, nil)
+	}
+
+	{
+		args := [?]string {"-unmodified-i:1"}
+		result := flags.parse(&s, args[:])
+		err, ok := result.(flags.Parse_Error)
+		testing.expectf(t, ok, "unexpected result: %v", result)
+		if ok {
+			testing.expect_value(t, err.reason, flags.Parse_Error_Reason.Unsupported_Type)
+		}
+	}
+}
+
+@(test)
+test_datetime :: proc(t: ^testing.T) {
+	when flags.IMPORTING_TIME {
+		W :: struct {
+			t: datetime.DateTime,
+		}
+		w: W
+
+		args := [?]string { "-t:2024-06-04T12:34:56Z" }
+		result := flags.parse(&w, args[:])
+		testing.expect_value(t, result, nil)
+		if result != nil {
+			return
+		}
+		testing.expect_value(t, w.t.date.year, 2024)
+		testing.expect_value(t, w.t.date.month, 6)
+		testing.expect_value(t, w.t.date.day, 4)
+	} else {
+		log.info("Skipping test due to lack of platform support.")
+	}
+}
+
+@(test)
+test_net :: proc(t: ^testing.T) {
+	when flags.IMPORTING_NET {
+		W :: struct {
+			addr: net.Host_Or_Endpoint,
+		}
+		w: W
+
+		args := [?]string { "-addr:odin-lang.org:80" }
+		result := flags.parse(&w, args[:])
+		testing.expect_value(t, result, nil)
+		if result != nil {
+			return
+		}
+		host, is_host := w.addr.(net.Host)
+		testing.expectf(t, is_host, "expected type of `addr` to be `net.Host`, was %v", w.addr)
+		testing.expect_value(t, host.hostname, "odin-lang.org")
+		testing.expect_value(t, host.port, 80)
+	} else {
+		log.info("Skipping test due to lack of platform support.")
+	}
+}
+
+@(test)
+test_custom_type_setter :: proc(t: ^testing.T) {
+	Custom_Bool :: distinct bool
+	Custom_Data :: struct {
+		a: int,
+	}
+
+	S :: struct {
+		a: Custom_Data,
+		b: Custom_Bool `args:"indistinct"`,
+	}
+	s: S
+
+	// NOTE: Mind that this setter is global state, and the test runner is multi-threaded.
+	// It should be fine so long as all type setter tests are in this one test proc.
+	flags.register_type_setter(proc (data: rawptr, data_type: typeid, _, _: string) -> (string, bool, runtime.Allocator_Error) {
+		if data_type == Custom_Data {
+			(cast(^Custom_Data)data).a = 32
+			return "", true, nil
+		}
+		return "", false, nil
+	})
+	defer flags.register_type_setter(nil)
+	args := [?]string { "-a:hellope", "-b:true" }
+	result := flags.parse(&s, args[:])
+	testing.expect_value(t, result, nil)
+	testing.expect_value(t, s.a.a, 32)
+	testing.expect_value(t, s.b, true)
+}
+
+// This test is sensitive to many of the underlying mechanisms of the library,
+// so if something isn't working, it'll probably show up here first, but it may
+// not be immediately obvious as to what's wrong.
+//
+// It makes for a good early warning system.
+@(test)
+test_usage_write_odin :: proc(t: ^testing.T) {
+	Expected_Output :: `Usage:
+	varg required-number [number] [name] -bars -bots -foos -gadgets -widgets [-array] [-count] [-greek] [-map-type] [-verbose] ...
+Flags:
+	-required-number:<int>, required  | some number
+	-number:<int>                     | some other number
+	-name:<string>
+		Multi-line documentation
+		gets formatted
+		very nicely.
+	-bars:<string>, exactly 3         | <This flag has not been documented yet.>
+	-bots:<string>, at least 1        | <This flag has not been documented yet.>
+	-foos:<string>, between 2 and 3   | <This flag has not been documented yet.>
+	-gadgets:<string>, at least 1     | <This flag has not been documented yet.>
+	-widgets:<string>, at most 2      | <This flag has not been documented yet.>
+	                                  |
+	-array:<rune>, multiple           | <This flag has not been documented yet.>
+	-count:<u8>                       | <This flag has not been documented yet.>
+	-greek:<Custom_Enum>              | <This flag has not been documented yet.>
+	-map-type:<cstring>=<u8>          | <This flag has not been documented yet.>
+	-verbose                          | <This flag has not been documented yet.>
+	<string, ...>                     | <This flag has not been documented yet.>
+`
+
+	Custom_Enum :: enum {
+		Alpha,
+		Omega,
+	}
+
+	S :: struct {
+		required_number: int `args:"pos=0,required" usage:"some number"`,
+		number: int `args:"pos=1" usage:"some other number"`,
+		name: string `args:"pos=2" usage:"
+	Multi-line documentation
+		gets formatted
+very nicely.
+
+"`,
+
+		c: u8 `args:"name=count"`,
+		greek: Custom_Enum,
+
+		array: [dynamic]rune,
+		map_type: map[cstring]byte,
+
+		gadgets: [dynamic]string `args:"required=1"`,
+		widgets: [dynamic]string `args:"required=<3"`,
+		foos: [dynamic]string `args:"required=2<4"`,
+		bars: [dynamic]string `args:"required=3<4"`,
+		bots: [dynamic]string `args:"required"`,
+
+		debug: bool `args:"hidden" usage:"print debug info"`,
+		verbose: bool,
+
+		varg: [dynamic]string,
+	}
+
+	builder := strings.builder_make()
+	defer strings.builder_destroy(&builder)
+	writer := strings.to_stream(&builder)
+	flags.write_usage(writer, S, "varg", .Odin)
+	testing.expect_value(t, strings.to_string(builder), Expected_Output)
+}
+
+@(test)
+test_usage_write_unix :: proc(t: ^testing.T) {
+	Expected_Output :: `Usage:
+	varg required-number [number] [name] --bars --bots --foos --gadgets --variadic-flag --widgets [--array] [--count] [--greek] [--verbose] ...
+Flags:
+	--required-number <int>, required       | some number
+	--number <int>                          | some other number
+	--name <string>
+		Multi-line documentation
+		gets formatted
+		very nicely.
+	--bars <string>, exactly 3              | <This flag has not been documented yet.>
+	--bots <string>, at least 1             | <This flag has not been documented yet.>
+	--foos <string>, between 2 and 3        | <This flag has not been documented yet.>
+	--gadgets <string>, at least 1          | <This flag has not been documented yet.>
+	--variadic-flag <int, ...>, at least 2  | <This flag has not been documented yet.>
+	--widgets <string>, at most 2           | <This flag has not been documented yet.>
+	                                        |
+	--array <rune>, multiple                | <This flag has not been documented yet.>
+	--count <u8>                            | <This flag has not been documented yet.>
+	--greek <Custom_Enum>                   | <This flag has not been documented yet.>
+	--verbose                               | <This flag has not been documented yet.>
+	<string, ...>                           | <This flag has not been documented yet.>
+`
+
+	Custom_Enum :: enum {
+		Alpha,
+		Omega,
+	}
+
+	S :: struct {
+		required_number: int `args:"pos=0,required" usage:"some number"`,
+		number: int `args:"pos=1" usage:"some other number"`,
+		name: string `args:"pos=2" usage:"
+	Multi-line documentation
+		gets formatted
+very nicely.
+
+"`,
+
+		c: u8 `args:"name=count"`,
+		greek: Custom_Enum,
+
+		array: [dynamic]rune,
+		variadic_flag: [dynamic]int `args:"variadic,required=2"`,
+
+		gadgets: [dynamic]string `args:"required=1"`,
+		widgets: [dynamic]string `args:"required=<3"`,
+		foos: [dynamic]string `args:"required=2<4"`,
+		bars: [dynamic]string `args:"required=3<4"`,
+		bots: [dynamic]string `args:"required"`,
+
+		debug: bool `args:"hidden" usage:"print debug info"`,
+		verbose: bool,
+
+		varg: [dynamic]string,
+	}
+
+	builder := strings.builder_make()
+	defer strings.builder_destroy(&builder)
+	writer := strings.to_stream(&builder)
+	flags.write_usage(writer, S, "varg", .Unix)
+	testing.expect_value(t, strings.to_string(builder), Expected_Output)
+}