Add core:flags

Based on the Feoramund's original package
This commit is contained in:
gingerBill
2024-06-04 19:08:03 +01:00
parent 3b7100f8e5
commit f4dd48aa5d
10 changed files with 904 additions and 0 deletions

28
core/flags/LICENSE Normal file
View File

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

124
core/flags/README.md Normal file
View File

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

15
core/flags/constants.odin Normal file
View File

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

157
core/flags/conversion.odin Normal file
View File

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

92
core/flags/doc.odin Normal file
View File

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

29
core/flags/errors.odin Normal file
View File

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

86
core/flags/parsing.odin Normal file
View File

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

139
core/flags/usage.odin Normal file
View File

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

189
core/flags/util.odin Normal file
View File

@@ -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
}

View File

@@ -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
}