aboutsummaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/flags/LICENSE28
-rw-r--r--core/flags/constants.odin38
-rw-r--r--core/flags/doc.odin181
-rw-r--r--core/flags/errors.odin58
-rw-r--r--core/flags/example/example.odin132
-rw-r--r--core/flags/internal_assignment.odin262
-rw-r--r--core/flags/internal_parsing.odin162
-rw-r--r--core/flags/internal_rtti.odin548
-rw-r--r--core/flags/internal_validation.odin243
-rw-r--r--core/flags/parsing.odin94
-rw-r--r--core/flags/rtti.odin43
-rw-r--r--core/flags/usage.odin293
-rw-r--r--core/flags/util.odin130
-rw-r--r--core/flags/validation.odin37
14 files changed, 2249 insertions, 0 deletions
diff --git a/core/flags/LICENSE b/core/flags/LICENSE
new file mode 100644
index 000000000..e4e21e62d
--- /dev/null
+++ b/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.
diff --git a/core/flags/constants.odin b/core/flags/constants.odin
new file mode 100644
index 000000000..ab3dc9a0a
--- /dev/null
+++ b/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
diff --git a/core/flags/doc.odin b/core/flags/doc.odin
new file mode 100644
index 000000000..c3663c419
--- /dev/null
+++ b/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
diff --git a/core/flags/errors.odin b/core/flags/errors.odin
new file mode 100644
index 000000000..1862a7a00
--- /dev/null
+++ b/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,
+}
diff --git a/core/flags/example/example.odin b/core/flags/example/example.odin
new file mode 100644
index 000000000..b1d7b58d6
--- /dev/null
+++ b/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")
+ }
+}
diff --git a/core/flags/internal_assignment.odin b/core/flags/internal_assignment.odin
new file mode 100644
index 000000000..1e715998d
--- /dev/null
+++ b/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),
+ }
+}
diff --git a/core/flags/internal_parsing.odin b/core/flags/internal_parsing.odin
new file mode 100644
index 000000000..349afdd29
--- /dev/null
+++ b/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
+}
diff --git a/core/flags/internal_rtti.odin b/core/flags/internal_rtti.odin
new file mode 100644
index 000000000..8a11ebbc1
--- /dev/null
+++ b/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
+}
diff --git a/core/flags/internal_validation.odin b/core/flags/internal_validation.odin
new file mode 100644
index 000000000..cfa1794cd
--- /dev/null
+++ b/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
+}
diff --git a/core/flags/parsing.odin b/core/flags/parsing.odin
new file mode 100644
index 000000000..d8aea513f
--- /dev/null
+++ b/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
+}
diff --git a/core/flags/rtti.odin b/core/flags/rtti.odin
new file mode 100644
index 000000000..ce7a23773
--- /dev/null
+++ b/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
+}
diff --git a/core/flags/usage.odin b/core/flags/usage.odin
new file mode 100644
index 000000000..48137b6cd
--- /dev/null
+++ b/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))
+}
diff --git a/core/flags/util.odin b/core/flags/util.odin
new file mode 100644
index 000000000..e4f32eea1
--- /dev/null
+++ b/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
+}
diff --git a/core/flags/validation.odin b/core/flags/validation.odin
new file mode 100644
index 000000000..e370cff48
--- /dev/null
+++ b/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
+}