Add package core:flags

This commit is contained in:
Feoramund
2024-06-06 18:35:43 -04:00
parent 08612423b9
commit edb685f04b
15 changed files with 3599 additions and 0 deletions

28
core/flags/LICENSE Normal file
View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

43
core/flags/rtti.odin Normal file
View File

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

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

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

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

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

View File

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

File diff suppressed because it is too large Load Diff