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