mirror of
https://github.com/odin-lang/Odin.git
synced 2025-12-28 17:04:34 +00:00
Add package core:flags
This commit is contained in:
28
core/flags/LICENSE
Normal file
28
core/flags/LICENSE
Normal 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
38
core/flags/constants.odin
Normal 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
181
core/flags/doc.odin
Normal 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
58
core/flags/errors.odin
Normal 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,
|
||||
}
|
||||
132
core/flags/example/example.odin
Normal file
132
core/flags/example/example.odin
Normal 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")
|
||||
}
|
||||
}
|
||||
262
core/flags/internal_assignment.odin
Normal file
262
core/flags/internal_assignment.odin
Normal 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),
|
||||
}
|
||||
}
|
||||
162
core/flags/internal_parsing.odin
Normal file
162
core/flags/internal_parsing.odin
Normal 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
|
||||
}
|
||||
548
core/flags/internal_rtti.odin
Normal file
548
core/flags/internal_rtti.odin
Normal 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
|
||||
}
|
||||
243
core/flags/internal_validation.odin
Normal file
243
core/flags/internal_validation.odin
Normal 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
94
core/flags/parsing.odin
Normal 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
43
core/flags/rtti.odin
Normal 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
293
core/flags/usage.odin
Normal 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
130
core/flags/util.odin
Normal 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
|
||||
}
|
||||
37
core/flags/validation.odin
Normal file
37
core/flags/validation.odin
Normal 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
|
||||
}
|
||||
1350
tests/core/flags/test_core_flags.odin
Normal file
1350
tests/core/flags/test_core_flags.odin
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user