mirror of
https://github.com/Ed94/Odin.git
synced 2026-07-27 09:50:03 +00:00
Add package core:flags
This commit is contained in:
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user