aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgingerBill <bill@gingerbill.org>2024-06-04 19:08:03 +0100
committergingerBill <bill@gingerbill.org>2024-06-04 19:08:03 +0100
commitf4dd48aa5d2d94b0daa72e4b51aae77774c49820 (patch)
tree55b714c62203eab2afc6a21d17d2f1c06b2a5cf6
parent3b7100f8e5cdf325a51926ba02f785a5443b0415 (diff)
Add `core:flags`
Based on the Feoramund's original package
-rw-r--r--core/flags/LICENSE28
-rw-r--r--core/flags/README.md124
-rw-r--r--core/flags/constants.odin15
-rw-r--r--core/flags/conversion.odin157
-rw-r--r--core/flags/doc.odin92
-rw-r--r--core/flags/errors.odin29
-rw-r--r--core/flags/parsing.odin86
-rw-r--r--core/flags/usage.odin139
-rw-r--r--core/flags/util.odin189
-rw-r--r--core/flags/validation.odin45
10 files changed, 904 insertions, 0 deletions
diff --git a/core/flags/LICENSE b/core/flags/LICENSE
new file mode 100644
index 000000000..5357227be
--- /dev/null
+++ b/core/flags/LICENSE
@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2024, Feoramund, Ginger Bill
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/core/flags/README.md b/core/flags/README.md
new file mode 100644
index 000000000..37dc1e9e4
--- /dev/null
+++ b/core/flags/README.md
@@ -0,0 +1,124 @@
+# `core:flags`
+
+`core:flags` is a complete command-line argument parser for the Odin programming
+language.
+
+It works by using Odin's run-time type information to determine where and how
+to store data on a struct provided by the user. Type conversion is handled
+automatically and errors are reported with useful messages.
+
+## Struct Tags
+
+Users of the `encoding/json` package may be familiar with using tags to
+annotate struct metadata. The same technique is used here to annotate where
+arguments should go and which are required.
+
+Under the `flags` tag:
+
+ - `name=S`, alias a struct field to `S`
+ - `pos=N`, place positional argument `N` into this field
+ - `hidden`, hide this field from the usage documentation
+ - `required`, cause verification to fail if this argument is not set
+
+There is also the `usage` tag, which is a plain string to be printed alongside
+the flag in the usage output.
+
+## Syntax
+
+Arguments are treated differently on how they're formatted. The format is
+similar to the Odin binary's way of handling compiler flags.
+
+```
+type handling
+------------ ------------------------
+<positional> depends on struct layout
+-<flag> set a bool to true
+-<flag:option> set flag to option
+-<flag=option> set flag to option, alternative syntax
+-<map>:<key>=<value> set map[key] to value
+```
+
+## Complete Example
+
+```odin
+package main
+
+import "core:fmt"
+import "core:mem"
+import "core:os"
+import "core:path/filepath"
+
+import "core:flags"
+
+main :: proc() {
+ Options :: struct {
+ file: string `flags:"pos=0,required" usage:"input file"`,
+ out: string `flags:"pos=1" usage:"output file"`,
+ retry_count: uint `flags:"name=retries" usage:"times to retry process"`,
+ debug: bool `flags:"hidden" usage:"print debug info"`,
+ collection: map[string]string `usage:"path aliases"`,
+ }
+
+ opt: Options
+ program: string
+ args: []string
+
+ switch len(os.args) {
+ case 0:
+ flags.print_usage(&opt)
+ os.exit(0)
+ case:
+ program = filepath.base(os.args[0])
+ args = os.args[1:]
+ }
+
+ err := flags.parse(&opt, args)
+
+ switch subtype in err {
+ case mem.Allocator_Error:
+ fmt.println("allocation error:", subtype)
+ os.exit(1)
+ case flags.Parse_Error:
+ fmt.println(subtype.message)
+ os.exit(1)
+ case flags.Validation_Error:
+ fmt.println(subtype.message)
+ os.exit(1)
+ case flags.Help_Request:
+ flags.print_usage(&opt, program)
+ os.exit(0)
+ }
+
+ fmt.printf("%#v\n", opt)
+}
+```
+
+```
+$ ./odin-flags
+required argument `file` was not set
+
+$ ./odin-flags -help
+
+Usage:
+ odin-flags file [out] [-collection] [-retries]
+Flags:
+ -file:<string> input file
+ -out:<string> output file
+ -collection:<string>=<string> path aliases
+ -retries:<uint> times to retry process
+
+$ ./odin-flags -retries:-3
+unable to set `retries` of type uint to `-3`
+
+$ ./odin-flags data -retries:3 -collection:core=./core -collection:runtime=./runtime
+Options{
+ file = "data",
+ out = "",
+ retry_count = 3,
+ debug = false,
+ collection = map[
+ core = "./core",
+ runtime = "./runtime",
+ ],
+}
+```
diff --git a/core/flags/constants.odin b/core/flags/constants.odin
new file mode 100644
index 000000000..d5df64a29
--- /dev/null
+++ b/core/flags/constants.odin
@@ -0,0 +1,15 @@
+package flags
+
+TAG_FLAGS :: "flags"
+SUBTAG_NAME :: "name"
+SUBTAG_POS :: "pos"
+SUBTAG_REQUIRED :: "required"
+SUBTAG_HIDDEN :: "hidden"
+
+TAG_USAGE :: "usage"
+
+MINIMUM_SPACING :: 4
+UNDOCUMENTED_FLAG :: "<This flag has not been documented yet.>"
+
+HARD_CODED_HELP_FLAG :: "help"
+HARD_CODED_HELP_FLAG_SHORT :: "h"
diff --git a/core/flags/conversion.odin b/core/flags/conversion.odin
new file mode 100644
index 000000000..226250228
--- /dev/null
+++ b/core/flags/conversion.odin
@@ -0,0 +1,157 @@
+package flags
+
+import "base:intrinsics"
+import "base:runtime"
+import "core:fmt"
+import "core:mem"
+import "core:reflect"
+
+_, _, _, _, _ :: intrinsics, runtime, fmt, mem, reflect
+
+// Add a positional argument to a data struct, checking for specified
+// positionals first before adding it to a fallback field.
+add_positional :: proc(data: ^$T, index: int, arg: string) -> Error {
+ field, has_pos_assigned := get_field_by_pos(data, index)
+
+ if !has_pos_assigned {
+ when !intrinsics.type_has_field(T, SUBTAG_POS) {
+ return Parse_Error {
+ .Extra_Pos,
+ fmt.tprintf("got extra positional argument `%s` with nowhere to store it", arg),
+ }
+ }
+
+ // Fall back to adding it to a dynamic array named `pos`.
+ field = reflect.struct_field_by_name(T, SUBTAG_POS)
+ assert(field.type != nil, "this should never happen")
+ }
+
+ ptr := cast(rawptr)(uintptr(data) + field.offset)
+ if !parse_and_set_pointer_by_type(ptr, arg, field.type) {
+ return Parse_Error {
+ .Bad_Type,
+ fmt.tprintf("unable to set positional %i (%s) of type %v to `%s`", index, field.name, field.type, arg),
+ }
+ }
+
+ return nil
+}
+
+// Set a `-flag` argument.
+set_flag :: proc(data: ^$T, name: string) -> Error {
+ // We make a special case for help requests.
+ switch name {
+ case HARD_CODED_HELP_FLAG:
+ fallthrough
+ case HARD_CODED_HELP_FLAG_SHORT:
+ return Help_Request{}
+ }
+
+ field := get_field_by_name(data, name) or_return
+
+ #partial switch t in field.type.variant {
+ case runtime.Type_Info_Boolean:
+ ptr := cast(^bool)(uintptr(data) + field.offset)
+ ptr^ = true
+ case:
+ return Parse_Error {
+ .Bad_Type,
+ fmt.tprintf("unable to set `%s` of type %v to true", name, field.type),
+ }
+ }
+
+ return nil
+}
+
+// Set a `-flag:option` argument.
+set_option :: proc(data: ^$T, name, option: string) -> Error {
+ field := get_field_by_name(data, name) or_return
+
+ // Guard against incorrect syntax.
+ #partial switch t in field.type.variant {
+ case runtime.Type_Info_Map:
+ return Parse_Error {
+ .Missing_Value,
+ fmt.tprintf("unable to set `%s` of type %v to `%s`, are you missing an `=`?", name, field.type, option),
+ }
+ }
+
+ ptr := rawptr(uintptr(data) + field.offset)
+ if !parse_and_set_pointer_by_type(ptr, option, field.type) {
+ return Parse_Error {
+ .Bad_Type,
+ fmt.tprintf("unable to set `%s` of type %v to `%s`", name, field.type, option),
+ }
+ }
+
+ return nil
+}
+
+// Set a `-map:key=value` argument.
+set_key_value :: proc(data: ^$T, name, key, value: string) -> Error {
+ field := get_field_by_name(data, name) or_return
+
+ #partial switch t in field.type.variant {
+ case runtime.Type_Info_Map:
+ if !reflect.is_string(t.key) {
+ return Parse_Error {
+ .Bad_Type,
+ fmt.tprintf("`%s` must be a map[string]", name),
+ }
+ }
+
+ key := key
+ key_ptr := rawptr(&key)
+ key_cstr: cstring
+ if reflect.is_cstring(t.key) {
+ key_cstr = cstring(raw_data(key))
+ key_ptr = &key_cstr
+ }
+
+ raw_map := (^runtime.Raw_Map)(uintptr(data) + field.offset)
+
+ hash := t.map_info.key_hasher(key_ptr, runtime.map_seed(raw_map^))
+
+ backing_alloc := false
+ elem_backing: []byte
+ value_ptr: rawptr
+
+ if raw_map.allocator.procedure == nil {
+ raw_map.allocator = context.allocator
+ } else {
+ value_ptr = runtime.__dynamic_map_get(raw_map,
+ t.map_info,
+ hash,
+ key_ptr,
+ )
+ }
+
+ if value_ptr == nil {
+ elem_backing = mem.alloc_bytes(t.value.size, t.value.align) or_return
+ backing_alloc = true
+ value_ptr = raw_data(elem_backing)
+ }
+
+ if !parse_and_set_pointer_by_type(value_ptr, value, t.value) {
+ break
+ }
+
+ if backing_alloc {
+ runtime.__dynamic_map_set(raw_map,
+ t.map_info,
+ hash,
+ key_ptr,
+ value_ptr,
+ )
+
+ delete(elem_backing)
+ }
+
+ return nil
+ }
+
+ return Parse_Error {
+ .Bad_Type,
+ fmt.tprintf("unable to set `%s` of type %v with key=value `%s` = `%s`", name, field.type, key, value),
+ }
+}
diff --git a/core/flags/doc.odin b/core/flags/doc.odin
new file mode 100644
index 000000000..c842050aa
--- /dev/null
+++ b/core/flags/doc.odin
@@ -0,0 +1,92 @@
+/*
+package flags implements a command-line argument parser.
+
+It works by using Odin's run-time type information to determine where and how
+to store data on a struct provided by the user. Type conversion is handled
+automatically and errors are reported with useful messages.
+
+
+Command-Line Syntax:
+
+Arguments are treated differently on how they're formatted. The format is
+similar to the Odin binary's way of handling compiler flags.
+
+```
+type handling
+------------ ------------------------
+<positional> depends on struct layout
+-<flag> set a bool true
+-<flag:option> set flag to option
+-<flag=option> set flag to option, alternative syntax
+-<map>:<key>=<value> set map[key] to value
+```
+
+
+Struct Tags:
+
+Users of the `encoding/json` package may be familiar with using tags to
+annotate struct metadata. The same technique is used here to annotate where
+arguments should go and which are required.
+
+Under the `args` tag:
+
+ - `name=S`, alias a struct field to `S`
+ - `pos=N`, place positional argument `N` into this field
+ - `hidden`, hide this field from the usage documentation
+ - `required`, cause verification to fail if this argument is not set
+
+There is also the `usage` tag, which is a plain string to be printed alongside
+the flag in the usage output.
+
+
+Supported Field Datatypes:
+
+- all `bool`s
+- all `int`s
+- all `float`s
+- `string`, `cstring`
+- `rune`
+- `dynamic` arrays with element types of the above
+- `map[string]`s with value types of the above
+
+
+Validation:
+
+The parser will ensure `required` arguments are set. This is on by default.
+
+
+Strict:
+
+The parser will return on the first error and stop parsing. This is on by
+default. Otherwise, all arguments that can be parsed, will be, and only the
+last error is returned.
+
+
+Help:
+
+By default, `-h` and `-help` are reserved flags which raise their own error
+type when set, allowing the program to handle the request differently from
+other errors.
+
+
+Example:
+
+```odin
+ Options :: struct {
+ file: string `args:"pos=0,required" usage:"input file"`,
+ out: string `args:"pos=1" usage:"output file"`,
+ retry_count: uint `args:"name=retries" usage:"times to retry process"`,
+ debug: bool `args:"hidden" usage:"print debug info"`,
+ collection: map[string]string `usage:"path aliases"`,
+ }
+
+ opt: Options
+ flags.parse(&opt, {
+ "main.odin",
+ "-retries:3",
+ "-collection:core=./core",
+ "-debug",
+ }, validate_args = true, strict = true)
+```
+*/
+package flags
diff --git a/core/flags/errors.odin b/core/flags/errors.odin
new file mode 100644
index 000000000..6acd26c20
--- /dev/null
+++ b/core/flags/errors.odin
@@ -0,0 +1,29 @@
+package flags
+
+import "base:runtime"
+
+Parse_Error_Type :: enum {
+ None,
+ Extra_Pos,
+ Bad_Type,
+ Missing_Field,
+ Missing_Value,
+}
+
+Parse_Error :: struct {
+ type: Parse_Error_Type,
+ message: string,
+}
+
+Validation_Error :: struct {
+ message: string,
+}
+
+Help_Request :: distinct bool
+
+Error :: union {
+ runtime.Allocator_Error,
+ Parse_Error,
+ Validation_Error,
+ Help_Request,
+}
diff --git a/core/flags/parsing.odin b/core/flags/parsing.odin
new file mode 100644
index 000000000..68f3ea56e
--- /dev/null
+++ b/core/flags/parsing.odin
@@ -0,0 +1,86 @@
+package flags
+
+import "core:strings"
+_ :: strings
+
+@(private)
+parse_one_arg :: proc(data: ^$T, arg: string, pos: ^int, set_args: ^[dynamic]string) -> (err: Error) {
+ arg := arg
+
+ if strings.has_prefix(arg, "-") {
+ arg = arg[1:]
+
+ if colon := strings.index_byte(arg, ':'); colon != -1 {
+ flag := arg[:colon]
+ arg = arg[1 + colon:]
+
+ if equals := strings.index_byte(arg, '='); equals != -1 {
+ // -map:key=value
+ key := arg[:equals]
+ value := arg[1 + equals:]
+ set_key_value(data, flag, key, value) or_return
+ append(set_args, flag)
+ } else {
+ // -flag:option
+ set_option(data, flag, arg) or_return
+ append(set_args, flag)
+ }
+
+ } else if equals := strings.index_byte(arg, '='); equals != -1 {
+ // -flag=option, alternative syntax
+ flag := arg[:equals]
+ arg = arg[1 + equals:]
+
+ set_option(data, flag, arg) or_return
+ append(set_args, flag)
+ } else {
+ // -flag
+ set_flag(data, arg) or_return
+ append(set_args, arg)
+ }
+
+ } else {
+ // positional
+ err = add_positional(data, pos^, arg)
+ pos^ += 1
+ }
+
+ return
+}
+
+// Parse a slice of command-line arguments into an annotated struct.
+//
+// If `validate_args` is set, an error will be returned if all required
+// arguments are not set. This step is only completed if there were no errors
+// from parsing.
+//
+// If `strict` is set, an error will cause parsing to stop and the procedure
+// will return with the message. Otherwise, parsing will continue and only the
+// last error will be returned.
+parse :: proc(data: ^$T, args: []string, validate_args: bool = true, strict: bool = true) -> (err: Error) {
+ // For checking required arguments.
+ set_args: [dynamic]string
+ defer delete(set_args)
+
+ // Positional argument tracker.
+ pos := 0
+
+ if strict {
+ for arg in args {
+ parse_one_arg(data, arg, &pos, &set_args) or_return
+ }
+ } else {
+ for arg in args {
+ this_error := parse_one_arg(data, arg, &pos, &set_args)
+ if this_error != nil {
+ err = this_error
+ }
+ }
+ }
+
+ if err == nil && validate_args {
+ return validate(data, pos, set_args[:])
+ }
+
+ return err
+}
diff --git a/core/flags/usage.odin b/core/flags/usage.odin
new file mode 100644
index 000000000..ebf440393
--- /dev/null
+++ b/core/flags/usage.odin
@@ -0,0 +1,139 @@
+package flags
+
+import "base:runtime"
+import "core:fmt"
+import "core:io"
+import "core:os"
+import "core:reflect"
+import "core:slice"
+import "core:strconv"
+import "core:strings"
+
+_, _, _, _, _, _, _, _ :: runtime, fmt, io, os, reflect, slice, strconv, strings
+
+// Write out the documentation for the command-line arguments.
+write_usage :: proc(out: io.Writer, data: ^$T, program: string = "") {
+ Flag :: struct {
+ name: string,
+ usage: string,
+ name_with_type: string,
+ pos: int,
+ is_positional: bool,
+ is_required: bool,
+ is_boolean: bool,
+ is_hidden: bool,
+ }
+
+ sort_flags :: proc(a, b: Flag) -> slice.Ordering {
+ if a.is_positional && b.is_positional {
+ return slice.cmp(a.pos, b.pos)
+ }
+
+ if a.is_required && !b.is_required {
+ return .Less
+ } else if !a.is_required && b.is_required {
+ return .Greater
+ }
+
+ if a.is_positional && !b.is_positional {
+ return .Less
+ } else if b.is_positional && !a.is_positional {
+ return .Greater
+ }
+
+ return slice.cmp(a.name, b.name)
+ }
+
+ flags: [dynamic]Flag
+ defer delete(flags)
+
+ longest_flag_length: int
+
+ for field in reflect.struct_fields_zipped(T) {
+ flag: Flag
+
+ flag.name = get_field_name(field)
+ #partial switch t in field.type.variant {
+ case runtime.Type_Info_Map:
+ flag.name_with_type = fmt.tprintf("%s:<%v>=<%v>", flag.name, t.key.id, t.value.id)
+ case runtime.Type_Info_Dynamic_Array:
+ flag.name_with_type = fmt.tprintf("%s:<%v, ...>", flag.name, t.elem.id)
+ case:
+ flag.name_with_type = fmt.tprintf("%s:<%v>", flag.name, field.type.id)
+ }
+
+ if usage, ok := reflect.struct_tag_lookup(field.tag, TAG_USAGE); ok {
+ flag.usage = usage
+ } else {
+ flag.usage = UNDOCUMENTED_FLAG
+ }
+
+ if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
+ if pos_str, is_pos := get_struct_subtag(args_tag, SUBTAG_POS); is_pos {
+ flag.is_positional = true
+ if pos, ok := strconv.parse_int(pos_str); ok && pos >= 0 {
+ flag.pos = pos
+ } else {
+ fmt.panicf("%v has incorrect pos subtag specifier `%s`", typeid_of(T), pos_str)
+ }
+ }
+ if _, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
+ flag.is_required = true
+ }
+ if reflect.type_kind(field.type.id) == .Boolean {
+ flag.is_boolean = true
+ }
+ if _, is_hidden := get_struct_subtag(args_tag, SUBTAG_HIDDEN); is_hidden {
+ flag.is_hidden = true
+ }
+ }
+
+ if !flag.is_hidden {
+ longest_flag_length = max(longest_flag_length, len(flag.name_with_type))
+ }
+
+ append(&flags, flag)
+ }
+
+ slice.sort_by_cmp(flags[:], sort_flags)
+
+ if len(program) > 0 {
+ fmt.wprintf(out, "Usage:\n\t%s", program)
+
+ for flag in flags {
+ if flag.is_hidden {
+ continue
+ }
+
+ io.write_byte(out, ' ')
+
+ if flag.name == SUBTAG_POS {
+ io.write_string(out, "...")
+ continue
+ }
+
+ if !flag.is_required { io.write_byte(out, '[') }
+ if !flag.is_positional { io.write_byte(out, '-') }
+ io.write_string(out, flag.name)
+ if !flag.is_required { io.write_byte(out, ']') }
+ }
+ io.write_byte(out, '\n')
+ }
+
+ fmt.wprintln(out, "Flags:")
+ for flag in flags {
+ if flag.is_hidden {
+ continue
+ }
+
+ spacing := strings.repeat(" ",
+ (MINIMUM_SPACING + longest_flag_length) - len(flag.name_with_type),
+ context.temp_allocator)
+ fmt.wprintf(out, "\t-%s%s%s\n", flag.name_with_type, spacing, flag.usage)
+ }
+}
+
+// Print out the documentation for the command-line arguments.
+print_usage :: proc(data: ^$T, program: string = "") {
+ write_usage(os.stream_from_handle(os.stdout), data, program)
+}
diff --git a/core/flags/util.odin b/core/flags/util.odin
new file mode 100644
index 000000000..fdce20272
--- /dev/null
+++ b/core/flags/util.odin
@@ -0,0 +1,189 @@
+package flags
+
+import "base:runtime"
+import "core:fmt"
+import "core:mem"
+import "core:reflect"
+import "core:strconv"
+import "core:strings"
+import "core:unicode/utf8"
+
+_, _, _, _, _, _, _ :: runtime, fmt, mem, reflect, strconv, strings, utf8
+
+@(private)
+parse_and_set_pointer_by_type :: proc(ptr: rawptr, value: string, ti: ^runtime.Type_Info) -> bool {
+ set_bool :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
+ (^T)(ptr)^ = (T)(strconv.parse_bool(str) or_return)
+ return true
+ }
+
+ set_i128 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
+ value := strconv.parse_i128(str) or_return
+ if value > cast(i128)max(T) || value < cast(i128)min(T) {
+ return false
+ }
+ (^T)(ptr)^ = (T)(value)
+ return true
+ }
+
+ set_u128 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
+ value := strconv.parse_u128(str) or_return
+ if value > cast(u128)max(T) {
+ return false
+ }
+ (^T)(ptr)^ = (T)(value)
+ return true
+ }
+
+ set_f64 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
+ (^T)(ptr)^ = (T)(strconv.parse_f64(str) or_return)
+ return true
+ }
+
+ a := any{ptr, ti.id}
+
+ #partial switch t in ti.variant {
+ case runtime.Type_Info_Dynamic_Array:
+ ptr := (^runtime.Raw_Dynamic_Array)(ptr)
+
+ // Try to convert the value first.
+ elem_backing, mem_err := mem.alloc_bytes(t.elem.size, t.elem.align)
+ if mem_err != nil {
+ return false
+ }
+ defer delete(elem_backing)
+ parse_and_set_pointer_by_type(raw_data(elem_backing), value, t.elem) or_return
+
+ runtime.__dynamic_array_resize(ptr, t.elem.size, t.elem.align, ptr.len + 1) or_return
+ subptr := cast(rawptr)(uintptr(ptr.data) + uintptr((ptr.len - 1) * t.elem.size))
+ mem.copy(subptr, raw_data(elem_backing), len(elem_backing))
+
+ case runtime.Type_Info_Boolean:
+ switch b in a {
+ case bool: set_bool(ptr, bool, value) or_return
+ case b8: set_bool(ptr, b8, value) or_return
+ case b16: set_bool(ptr, b16, value) or_return
+ case b32: set_bool(ptr, b32, value) or_return
+ case b64: set_bool(ptr, b64, value) or_return
+ }
+
+ case runtime.Type_Info_Rune:
+ r := utf8.rune_at_pos(value, 0)
+ if r == utf8.RUNE_ERROR { return false }
+ (^rune)(ptr)^ = r
+
+ case runtime.Type_Info_String:
+ switch s in a {
+ case string: (^string)(ptr)^ = value
+ case cstring: (^cstring)(ptr)^ = strings.clone_to_cstring(value)
+ }
+ case runtime.Type_Info_Integer:
+ switch i in a {
+ case int: set_i128(ptr, int, value) or_return
+ case i8: set_i128(ptr, i8, value) or_return
+ case i16: set_i128(ptr, i16, value) or_return
+ case i32: set_i128(ptr, i32, value) or_return
+ case i64: set_i128(ptr, i64, value) or_return
+ case i128: set_i128(ptr, i128, value) or_return
+ case i16le: set_i128(ptr, i16le, value) or_return
+ case i32le: set_i128(ptr, i32le, value) or_return
+ case i64le: set_i128(ptr, i64le, value) or_return
+ case i128le: set_i128(ptr, i128le, value) or_return
+ case i16be: set_i128(ptr, i16be, value) or_return
+ case i32be: set_i128(ptr, i32be, value) or_return
+ case i64be: set_i128(ptr, i64be, value) or_return
+ case i128be: set_i128(ptr, i128be, value) or_return
+
+ case uint: set_u128(ptr, uint, value) or_return
+ case uintptr: set_u128(ptr, uintptr, value) or_return
+ case u8: set_u128(ptr, u8, value) or_return
+ case u16: set_u128(ptr, u16, value) or_return
+ case u32: set_u128(ptr, u32, value) or_return
+ case u64: set_u128(ptr, u64, value) or_return
+ case u128: set_u128(ptr, u128, value) or_return
+ case u16le: set_u128(ptr, u16le, value) or_return
+ case u32le: set_u128(ptr, u32le, value) or_return
+ case u64le: set_u128(ptr, u64le, value) or_return
+ case u128le: set_u128(ptr, u128le, value) or_return
+ case u16be: set_u128(ptr, u16be, value) or_return
+ case u32be: set_u128(ptr, u32be, value) or_return
+ case u64be: set_u128(ptr, u64be, value) or_return
+ case u128be: set_u128(ptr, u128be, value) or_return
+ }
+ case runtime.Type_Info_Float:
+ switch f in a {
+ case f16: set_f64(ptr, f16, value) or_return
+ case f32: set_f64(ptr, f32, value) or_return
+ case f64: set_f64(ptr, f64, value) or_return
+
+ case f16le: set_f64(ptr, f16le, value) or_return
+ case f32le: set_f64(ptr, f32le, value) or_return
+ case f64le: set_f64(ptr, f64le, value) or_return
+
+ case f16be: set_f64(ptr, f16be, value) or_return
+ case f32be: set_f64(ptr, f32be, value) or_return
+ case f64be: set_f64(ptr, f64be, value) or_return
+ }
+ case:
+ return false
+ }
+
+ return true
+}
+
+@(private)
+get_struct_subtag :: proc(tag, id: string) -> (value: string, ok: bool) {
+ tag := tag
+
+ for subtag in strings.split_iterator(&tag, ",") {
+ if equals := strings.index_byte(subtag, '='); equals != -1 && id == subtag[:equals] {
+ return subtag[1 + equals:], true
+ } else if id == subtag {
+ return "", true
+ }
+ }
+
+ return "", false
+}
+
+@(private)
+get_field_name :: proc(field: reflect.Struct_Field) -> string {
+ if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_FLAGS); ok {
+ if name_subtag, name_ok := get_struct_subtag(args_tag, SUBTAG_NAME); name_ok {
+ return name_subtag
+ }
+ }
+
+ return field.name
+}
+
+// Get a struct field by its field name or "name" subtag.
+// NOTE: `Error` uses the `context.temp_allocator` to give context about the error message
+get_field_by_name :: proc(data: ^$T, name: string) -> (field: reflect.Struct_Field, err: Error) {
+ for field in reflect.struct_fields_zipped(T) {
+ if get_field_name(field) == name {
+ return field, nil
+ }
+ }
+
+ return {}, Parse_Error {
+ .Missing_Field,
+ fmt.tprintf("unable to find argument by name `%s`", name),
+ }
+}
+
+// Get a struct field by its "pos" subtag.
+get_field_by_pos :: proc(data: ^$T, index: int) -> (field: reflect.Struct_Field, ok: bool) {
+ fields := reflect.struct_fields_zipped(T)
+
+ for field in fields {
+ args_tag := reflect.struct_tag_lookup(field.tag, TAG_FLAGS) or_continue
+ pos_subtag := get_struct_subtag(args_tag, SUBTAG_POS) or_continue
+ value := strconv.parse_int(pos_subtag) or_continue
+ if value == index {
+ return field, true
+ }
+ }
+
+ return {}, false
+}
diff --git a/core/flags/validation.odin b/core/flags/validation.odin
new file mode 100644
index 000000000..c5cad8c7a
--- /dev/null
+++ b/core/flags/validation.odin
@@ -0,0 +1,45 @@
+package flags
+
+import "core:fmt"
+import "core:reflect"
+import "core:strconv"
+
+_ :: fmt
+_ :: reflect
+_ :: strconv
+
+// Validate that all the required arguments are set.
+validate :: proc(data: ^$T, max_pos: int, set_args: []string) -> Error {
+ fields := reflect.struct_fields_zipped(T)
+
+ check_fields: for field in fields {
+ tag := reflect.struct_tag_lookup(field.tag, TAG_ARGS) or_continue
+ if _, ok := get_struct_subtag(tag, SUBTAG_REQUIRED); ok {
+ was_set := false
+
+ // Check if it was set by name.
+ check_set_args: for set_arg in set_args {
+ if get_field_name(field) == set_arg {
+ was_set = true
+ break check_set_args
+ }
+ }
+
+ // Check if it was set by position.
+ if pos, has_pos := get_struct_subtag(tag, SUBTAG_POS); has_pos {
+ value, value_ok := strconv.parse_int(pos)
+ if value < max_pos {
+ was_set = true
+ }
+ }
+
+ if !was_set {
+ return Validation_Error {
+ fmt.tprintf("required argument `%s` was not set", field.name),
+ }
+ }
+ }
+ }
+
+ return nil
+}