mirror of
https://github.com/odin-lang/Odin.git
synced 2026-06-14 14:23:43 +00:00
Add core:flags
Based on the Feoramund's original package
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, Ginger Bill
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
124
core/flags/README.md
Normal file
124
core/flags/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# `core:flags`
|
||||
|
||||
`core:flags` is a complete command-line argument parser for the Odin programming
|
||||
language.
|
||||
|
||||
It works by using Odin's run-time type information to determine where and how
|
||||
to store data on a struct provided by the user. Type conversion is handled
|
||||
automatically and errors are reported with useful messages.
|
||||
|
||||
## Struct Tags
|
||||
|
||||
Users of the `encoding/json` package may be familiar with using tags to
|
||||
annotate struct metadata. The same technique is used here to annotate where
|
||||
arguments should go and which are required.
|
||||
|
||||
Under the `flags` tag:
|
||||
|
||||
- `name=S`, alias a struct field to `S`
|
||||
- `pos=N`, place positional argument `N` into this field
|
||||
- `hidden`, hide this field from the usage documentation
|
||||
- `required`, cause verification to fail if this argument is not set
|
||||
|
||||
There is also the `usage` tag, which is a plain string to be printed alongside
|
||||
the flag in the usage output.
|
||||
|
||||
## Syntax
|
||||
|
||||
Arguments are treated differently on how they're formatted. The format is
|
||||
similar to the Odin binary's way of handling compiler flags.
|
||||
|
||||
```
|
||||
type handling
|
||||
------------ ------------------------
|
||||
<positional> depends on struct layout
|
||||
-<flag> set a bool to true
|
||||
-<flag:option> set flag to option
|
||||
-<flag=option> set flag to option, alternative syntax
|
||||
-<map>:<key>=<value> set map[key] to value
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```odin
|
||||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
import "core:path/filepath"
|
||||
|
||||
import "core:flags"
|
||||
|
||||
main :: proc() {
|
||||
Options :: struct {
|
||||
file: string `flags:"pos=0,required" usage:"input file"`,
|
||||
out: string `flags:"pos=1" usage:"output file"`,
|
||||
retry_count: uint `flags:"name=retries" usage:"times to retry process"`,
|
||||
debug: bool `flags:"hidden" usage:"print debug info"`,
|
||||
collection: map[string]string `usage:"path aliases"`,
|
||||
}
|
||||
|
||||
opt: Options
|
||||
program: string
|
||||
args: []string
|
||||
|
||||
switch len(os.args) {
|
||||
case 0:
|
||||
flags.print_usage(&opt)
|
||||
os.exit(0)
|
||||
case:
|
||||
program = filepath.base(os.args[0])
|
||||
args = os.args[1:]
|
||||
}
|
||||
|
||||
err := flags.parse(&opt, args)
|
||||
|
||||
switch subtype in err {
|
||||
case mem.Allocator_Error:
|
||||
fmt.println("allocation error:", subtype)
|
||||
os.exit(1)
|
||||
case flags.Parse_Error:
|
||||
fmt.println(subtype.message)
|
||||
os.exit(1)
|
||||
case flags.Validation_Error:
|
||||
fmt.println(subtype.message)
|
||||
os.exit(1)
|
||||
case flags.Help_Request:
|
||||
flags.print_usage(&opt, program)
|
||||
os.exit(0)
|
||||
}
|
||||
|
||||
fmt.printf("%#v\n", opt)
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
$ ./odin-flags
|
||||
required argument `file` was not set
|
||||
|
||||
$ ./odin-flags -help
|
||||
|
||||
Usage:
|
||||
odin-flags file [out] [-collection] [-retries]
|
||||
Flags:
|
||||
-file:<string> input file
|
||||
-out:<string> output file
|
||||
-collection:<string>=<string> path aliases
|
||||
-retries:<uint> times to retry process
|
||||
|
||||
$ ./odin-flags -retries:-3
|
||||
unable to set `retries` of type uint to `-3`
|
||||
|
||||
$ ./odin-flags data -retries:3 -collection:core=./core -collection:runtime=./runtime
|
||||
Options{
|
||||
file = "data",
|
||||
out = "",
|
||||
retry_count = 3,
|
||||
debug = false,
|
||||
collection = map[
|
||||
core = "./core",
|
||||
runtime = "./runtime",
|
||||
],
|
||||
}
|
||||
```
|
||||
15
core/flags/constants.odin
Normal file
15
core/flags/constants.odin
Normal file
@@ -0,0 +1,15 @@
|
||||
package flags
|
||||
|
||||
TAG_FLAGS :: "flags"
|
||||
SUBTAG_NAME :: "name"
|
||||
SUBTAG_POS :: "pos"
|
||||
SUBTAG_REQUIRED :: "required"
|
||||
SUBTAG_HIDDEN :: "hidden"
|
||||
|
||||
TAG_USAGE :: "usage"
|
||||
|
||||
MINIMUM_SPACING :: 4
|
||||
UNDOCUMENTED_FLAG :: "<This flag has not been documented yet.>"
|
||||
|
||||
HARD_CODED_HELP_FLAG :: "help"
|
||||
HARD_CODED_HELP_FLAG_SHORT :: "h"
|
||||
157
core/flags/conversion.odin
Normal file
157
core/flags/conversion.odin
Normal file
@@ -0,0 +1,157 @@
|
||||
package flags
|
||||
|
||||
import "base:intrinsics"
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:reflect"
|
||||
|
||||
_, _, _, _, _ :: intrinsics, runtime, fmt, mem, reflect
|
||||
|
||||
// Add a positional argument to a data struct, checking for specified
|
||||
// positionals first before adding it to a fallback field.
|
||||
add_positional :: proc(data: ^$T, index: int, arg: string) -> Error {
|
||||
field, has_pos_assigned := get_field_by_pos(data, index)
|
||||
|
||||
if !has_pos_assigned {
|
||||
when !intrinsics.type_has_field(T, SUBTAG_POS) {
|
||||
return Parse_Error {
|
||||
.Extra_Pos,
|
||||
fmt.tprintf("got extra positional argument `%s` with nowhere to store it", arg),
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to adding it to a dynamic array named `pos`.
|
||||
field = reflect.struct_field_by_name(T, SUBTAG_POS)
|
||||
assert(field.type != nil, "this should never happen")
|
||||
}
|
||||
|
||||
ptr := cast(rawptr)(uintptr(data) + field.offset)
|
||||
if !parse_and_set_pointer_by_type(ptr, arg, field.type) {
|
||||
return Parse_Error {
|
||||
.Bad_Type,
|
||||
fmt.tprintf("unable to set positional %i (%s) of type %v to `%s`", index, field.name, field.type, arg),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set a `-flag` argument.
|
||||
set_flag :: proc(data: ^$T, name: string) -> Error {
|
||||
// We make a special case for help requests.
|
||||
switch name {
|
||||
case HARD_CODED_HELP_FLAG:
|
||||
fallthrough
|
||||
case HARD_CODED_HELP_FLAG_SHORT:
|
||||
return Help_Request{}
|
||||
}
|
||||
|
||||
field := get_field_by_name(data, name) or_return
|
||||
|
||||
#partial switch t in field.type.variant {
|
||||
case runtime.Type_Info_Boolean:
|
||||
ptr := cast(^bool)(uintptr(data) + field.offset)
|
||||
ptr^ = true
|
||||
case:
|
||||
return Parse_Error {
|
||||
.Bad_Type,
|
||||
fmt.tprintf("unable to set `%s` of type %v to true", name, field.type),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set a `-flag:option` argument.
|
||||
set_option :: proc(data: ^$T, name, option: string) -> Error {
|
||||
field := get_field_by_name(data, name) or_return
|
||||
|
||||
// Guard against incorrect syntax.
|
||||
#partial switch t in field.type.variant {
|
||||
case runtime.Type_Info_Map:
|
||||
return Parse_Error {
|
||||
.Missing_Value,
|
||||
fmt.tprintf("unable to set `%s` of type %v to `%s`, are you missing an `=`?", name, field.type, option),
|
||||
}
|
||||
}
|
||||
|
||||
ptr := rawptr(uintptr(data) + field.offset)
|
||||
if !parse_and_set_pointer_by_type(ptr, option, field.type) {
|
||||
return Parse_Error {
|
||||
.Bad_Type,
|
||||
fmt.tprintf("unable to set `%s` of type %v to `%s`", name, field.type, option),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set a `-map:key=value` argument.
|
||||
set_key_value :: proc(data: ^$T, name, key, value: string) -> Error {
|
||||
field := get_field_by_name(data, name) or_return
|
||||
|
||||
#partial switch t in field.type.variant {
|
||||
case runtime.Type_Info_Map:
|
||||
if !reflect.is_string(t.key) {
|
||||
return Parse_Error {
|
||||
.Bad_Type,
|
||||
fmt.tprintf("`%s` must be a map[string]", name),
|
||||
}
|
||||
}
|
||||
|
||||
key := key
|
||||
key_ptr := rawptr(&key)
|
||||
key_cstr: cstring
|
||||
if reflect.is_cstring(t.key) {
|
||||
key_cstr = cstring(raw_data(key))
|
||||
key_ptr = &key_cstr
|
||||
}
|
||||
|
||||
raw_map := (^runtime.Raw_Map)(uintptr(data) + field.offset)
|
||||
|
||||
hash := t.map_info.key_hasher(key_ptr, runtime.map_seed(raw_map^))
|
||||
|
||||
backing_alloc := false
|
||||
elem_backing: []byte
|
||||
value_ptr: rawptr
|
||||
|
||||
if raw_map.allocator.procedure == nil {
|
||||
raw_map.allocator = context.allocator
|
||||
} else {
|
||||
value_ptr = runtime.__dynamic_map_get(raw_map,
|
||||
t.map_info,
|
||||
hash,
|
||||
key_ptr,
|
||||
)
|
||||
}
|
||||
|
||||
if value_ptr == nil {
|
||||
elem_backing = mem.alloc_bytes(t.value.size, t.value.align) or_return
|
||||
backing_alloc = true
|
||||
value_ptr = raw_data(elem_backing)
|
||||
}
|
||||
|
||||
if !parse_and_set_pointer_by_type(value_ptr, value, t.value) {
|
||||
break
|
||||
}
|
||||
|
||||
if backing_alloc {
|
||||
runtime.__dynamic_map_set(raw_map,
|
||||
t.map_info,
|
||||
hash,
|
||||
key_ptr,
|
||||
value_ptr,
|
||||
)
|
||||
|
||||
delete(elem_backing)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return Parse_Error {
|
||||
.Bad_Type,
|
||||
fmt.tprintf("unable to set `%s` of type %v with key=value `%s` = `%s`", name, field.type, key, value),
|
||||
}
|
||||
}
|
||||
92
core/flags/doc.odin
Normal file
92
core/flags/doc.odin
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
package flags implements a command-line argument parser.
|
||||
|
||||
It works by using Odin's run-time type information to determine where and how
|
||||
to store data on a struct provided by the user. Type conversion is handled
|
||||
automatically and errors are reported with useful messages.
|
||||
|
||||
|
||||
Command-Line Syntax:
|
||||
|
||||
Arguments are treated differently on how they're formatted. The format is
|
||||
similar to the Odin binary's way of handling compiler flags.
|
||||
|
||||
```
|
||||
type handling
|
||||
------------ ------------------------
|
||||
<positional> depends on struct layout
|
||||
-<flag> set a bool true
|
||||
-<flag:option> set flag to option
|
||||
-<flag=option> set flag to option, alternative syntax
|
||||
-<map>:<key>=<value> set map[key] to value
|
||||
```
|
||||
|
||||
|
||||
Struct Tags:
|
||||
|
||||
Users of the `encoding/json` package may be familiar with using tags to
|
||||
annotate struct metadata. The same technique is used here to annotate where
|
||||
arguments should go and which are required.
|
||||
|
||||
Under the `args` tag:
|
||||
|
||||
- `name=S`, alias a struct field to `S`
|
||||
- `pos=N`, place positional argument `N` into this field
|
||||
- `hidden`, hide this field from the usage documentation
|
||||
- `required`, cause verification to fail if this argument is not set
|
||||
|
||||
There is also the `usage` tag, which is a plain string to be printed alongside
|
||||
the flag in the usage output.
|
||||
|
||||
|
||||
Supported Field Datatypes:
|
||||
|
||||
- all `bool`s
|
||||
- all `int`s
|
||||
- all `float`s
|
||||
- `string`, `cstring`
|
||||
- `rune`
|
||||
- `dynamic` arrays with element types of the above
|
||||
- `map[string]`s with value types of the above
|
||||
|
||||
|
||||
Validation:
|
||||
|
||||
The parser will ensure `required` arguments are set. This is on by default.
|
||||
|
||||
|
||||
Strict:
|
||||
|
||||
The parser will return on the first error and stop parsing. This is on by
|
||||
default. Otherwise, all arguments that can be parsed, will be, and only the
|
||||
last error is returned.
|
||||
|
||||
|
||||
Help:
|
||||
|
||||
By default, `-h` and `-help` are reserved flags which raise their own error
|
||||
type when set, allowing the program to handle the request differently from
|
||||
other errors.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
```odin
|
||||
Options :: struct {
|
||||
file: string `args:"pos=0,required" usage:"input file"`,
|
||||
out: string `args:"pos=1" usage:"output file"`,
|
||||
retry_count: uint `args:"name=retries" usage:"times to retry process"`,
|
||||
debug: bool `args:"hidden" usage:"print debug info"`,
|
||||
collection: map[string]string `usage:"path aliases"`,
|
||||
}
|
||||
|
||||
opt: Options
|
||||
flags.parse(&opt, {
|
||||
"main.odin",
|
||||
"-retries:3",
|
||||
"-collection:core=./core",
|
||||
"-debug",
|
||||
}, validate_args = true, strict = true)
|
||||
```
|
||||
*/
|
||||
package flags
|
||||
29
core/flags/errors.odin
Normal file
29
core/flags/errors.odin
Normal file
@@ -0,0 +1,29 @@
|
||||
package flags
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
Parse_Error_Type :: enum {
|
||||
None,
|
||||
Extra_Pos,
|
||||
Bad_Type,
|
||||
Missing_Field,
|
||||
Missing_Value,
|
||||
}
|
||||
|
||||
Parse_Error :: struct {
|
||||
type: Parse_Error_Type,
|
||||
message: string,
|
||||
}
|
||||
|
||||
Validation_Error :: struct {
|
||||
message: string,
|
||||
}
|
||||
|
||||
Help_Request :: distinct bool
|
||||
|
||||
Error :: union {
|
||||
runtime.Allocator_Error,
|
||||
Parse_Error,
|
||||
Validation_Error,
|
||||
Help_Request,
|
||||
}
|
||||
86
core/flags/parsing.odin
Normal file
86
core/flags/parsing.odin
Normal file
@@ -0,0 +1,86 @@
|
||||
package flags
|
||||
|
||||
import "core:strings"
|
||||
_ :: strings
|
||||
|
||||
@(private)
|
||||
parse_one_arg :: proc(data: ^$T, arg: string, pos: ^int, set_args: ^[dynamic]string) -> (err: Error) {
|
||||
arg := arg
|
||||
|
||||
if strings.has_prefix(arg, "-") {
|
||||
arg = arg[1:]
|
||||
|
||||
if colon := strings.index_byte(arg, ':'); colon != -1 {
|
||||
flag := arg[:colon]
|
||||
arg = arg[1 + colon:]
|
||||
|
||||
if equals := strings.index_byte(arg, '='); equals != -1 {
|
||||
// -map:key=value
|
||||
key := arg[:equals]
|
||||
value := arg[1 + equals:]
|
||||
set_key_value(data, flag, key, value) or_return
|
||||
append(set_args, flag)
|
||||
} else {
|
||||
// -flag:option
|
||||
set_option(data, flag, arg) or_return
|
||||
append(set_args, flag)
|
||||
}
|
||||
|
||||
} else if equals := strings.index_byte(arg, '='); equals != -1 {
|
||||
// -flag=option, alternative syntax
|
||||
flag := arg[:equals]
|
||||
arg = arg[1 + equals:]
|
||||
|
||||
set_option(data, flag, arg) or_return
|
||||
append(set_args, flag)
|
||||
} else {
|
||||
// -flag
|
||||
set_flag(data, arg) or_return
|
||||
append(set_args, arg)
|
||||
}
|
||||
|
||||
} else {
|
||||
// positional
|
||||
err = add_positional(data, pos^, arg)
|
||||
pos^ += 1
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Parse a slice of command-line arguments into an annotated struct.
|
||||
//
|
||||
// If `validate_args` is set, an error will be returned if all required
|
||||
// arguments are not set. This step is only completed if there were no errors
|
||||
// from parsing.
|
||||
//
|
||||
// If `strict` is set, an error will cause parsing to stop and the procedure
|
||||
// will return with the message. Otherwise, parsing will continue and only the
|
||||
// last error will be returned.
|
||||
parse :: proc(data: ^$T, args: []string, validate_args: bool = true, strict: bool = true) -> (err: Error) {
|
||||
// For checking required arguments.
|
||||
set_args: [dynamic]string
|
||||
defer delete(set_args)
|
||||
|
||||
// Positional argument tracker.
|
||||
pos := 0
|
||||
|
||||
if strict {
|
||||
for arg in args {
|
||||
parse_one_arg(data, arg, &pos, &set_args) or_return
|
||||
}
|
||||
} else {
|
||||
for arg in args {
|
||||
this_error := parse_one_arg(data, arg, &pos, &set_args)
|
||||
if this_error != nil {
|
||||
err = this_error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && validate_args {
|
||||
return validate(data, pos, set_args[:])
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
139
core/flags/usage.odin
Normal file
139
core/flags/usage.odin
Normal file
@@ -0,0 +1,139 @@
|
||||
package flags
|
||||
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
import "core:io"
|
||||
import "core:os"
|
||||
import "core:reflect"
|
||||
import "core:slice"
|
||||
import "core:strconv"
|
||||
import "core:strings"
|
||||
|
||||
_, _, _, _, _, _, _, _ :: runtime, fmt, io, os, reflect, slice, strconv, strings
|
||||
|
||||
// Write out the documentation for the command-line arguments.
|
||||
write_usage :: proc(out: io.Writer, data: ^$T, program: string = "") {
|
||||
Flag :: struct {
|
||||
name: string,
|
||||
usage: string,
|
||||
name_with_type: string,
|
||||
pos: int,
|
||||
is_positional: bool,
|
||||
is_required: bool,
|
||||
is_boolean: bool,
|
||||
is_hidden: bool,
|
||||
}
|
||||
|
||||
sort_flags :: proc(a, b: Flag) -> slice.Ordering {
|
||||
if a.is_positional && b.is_positional {
|
||||
return slice.cmp(a.pos, b.pos)
|
||||
}
|
||||
|
||||
if a.is_required && !b.is_required {
|
||||
return .Less
|
||||
} else if !a.is_required && b.is_required {
|
||||
return .Greater
|
||||
}
|
||||
|
||||
if a.is_positional && !b.is_positional {
|
||||
return .Less
|
||||
} else if b.is_positional && !a.is_positional {
|
||||
return .Greater
|
||||
}
|
||||
|
||||
return slice.cmp(a.name, b.name)
|
||||
}
|
||||
|
||||
flags: [dynamic]Flag
|
||||
defer delete(flags)
|
||||
|
||||
longest_flag_length: int
|
||||
|
||||
for field in reflect.struct_fields_zipped(T) {
|
||||
flag: Flag
|
||||
|
||||
flag.name = get_field_name(field)
|
||||
#partial switch t in field.type.variant {
|
||||
case runtime.Type_Info_Map:
|
||||
flag.name_with_type = fmt.tprintf("%s:<%v>=<%v>", flag.name, t.key.id, t.value.id)
|
||||
case runtime.Type_Info_Dynamic_Array:
|
||||
flag.name_with_type = fmt.tprintf("%s:<%v, ...>", flag.name, t.elem.id)
|
||||
case:
|
||||
flag.name_with_type = fmt.tprintf("%s:<%v>", flag.name, field.type.id)
|
||||
}
|
||||
|
||||
if usage, ok := reflect.struct_tag_lookup(field.tag, TAG_USAGE); ok {
|
||||
flag.usage = usage
|
||||
} else {
|
||||
flag.usage = UNDOCUMENTED_FLAG
|
||||
}
|
||||
|
||||
if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
|
||||
if pos_str, is_pos := get_struct_subtag(args_tag, SUBTAG_POS); is_pos {
|
||||
flag.is_positional = true
|
||||
if pos, ok := strconv.parse_int(pos_str); ok && pos >= 0 {
|
||||
flag.pos = pos
|
||||
} else {
|
||||
fmt.panicf("%v has incorrect pos subtag specifier `%s`", typeid_of(T), pos_str)
|
||||
}
|
||||
}
|
||||
if _, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
|
||||
flag.is_required = true
|
||||
}
|
||||
if reflect.type_kind(field.type.id) == .Boolean {
|
||||
flag.is_boolean = true
|
||||
}
|
||||
if _, is_hidden := get_struct_subtag(args_tag, SUBTAG_HIDDEN); is_hidden {
|
||||
flag.is_hidden = true
|
||||
}
|
||||
}
|
||||
|
||||
if !flag.is_hidden {
|
||||
longest_flag_length = max(longest_flag_length, len(flag.name_with_type))
|
||||
}
|
||||
|
||||
append(&flags, flag)
|
||||
}
|
||||
|
||||
slice.sort_by_cmp(flags[:], sort_flags)
|
||||
|
||||
if len(program) > 0 {
|
||||
fmt.wprintf(out, "Usage:\n\t%s", program)
|
||||
|
||||
for flag in flags {
|
||||
if flag.is_hidden {
|
||||
continue
|
||||
}
|
||||
|
||||
io.write_byte(out, ' ')
|
||||
|
||||
if flag.name == SUBTAG_POS {
|
||||
io.write_string(out, "...")
|
||||
continue
|
||||
}
|
||||
|
||||
if !flag.is_required { io.write_byte(out, '[') }
|
||||
if !flag.is_positional { io.write_byte(out, '-') }
|
||||
io.write_string(out, flag.name)
|
||||
if !flag.is_required { io.write_byte(out, ']') }
|
||||
}
|
||||
io.write_byte(out, '\n')
|
||||
}
|
||||
|
||||
fmt.wprintln(out, "Flags:")
|
||||
for flag in flags {
|
||||
if flag.is_hidden {
|
||||
continue
|
||||
}
|
||||
|
||||
spacing := strings.repeat(" ",
|
||||
(MINIMUM_SPACING + longest_flag_length) - len(flag.name_with_type),
|
||||
context.temp_allocator)
|
||||
fmt.wprintf(out, "\t-%s%s%s\n", flag.name_with_type, spacing, flag.usage)
|
||||
}
|
||||
}
|
||||
|
||||
// Print out the documentation for the command-line arguments.
|
||||
print_usage :: proc(data: ^$T, program: string = "") {
|
||||
write_usage(os.stream_from_handle(os.stdout), data, program)
|
||||
}
|
||||
189
core/flags/util.odin
Normal file
189
core/flags/util.odin
Normal file
@@ -0,0 +1,189 @@
|
||||
package flags
|
||||
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:reflect"
|
||||
import "core:strconv"
|
||||
import "core:strings"
|
||||
import "core:unicode/utf8"
|
||||
|
||||
_, _, _, _, _, _, _ :: runtime, fmt, mem, reflect, strconv, strings, utf8
|
||||
|
||||
@(private)
|
||||
parse_and_set_pointer_by_type :: proc(ptr: rawptr, value: string, ti: ^runtime.Type_Info) -> bool {
|
||||
set_bool :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
|
||||
(^T)(ptr)^ = (T)(strconv.parse_bool(str) or_return)
|
||||
return true
|
||||
}
|
||||
|
||||
set_i128 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
|
||||
value := strconv.parse_i128(str) or_return
|
||||
if value > cast(i128)max(T) || value < cast(i128)min(T) {
|
||||
return false
|
||||
}
|
||||
(^T)(ptr)^ = (T)(value)
|
||||
return true
|
||||
}
|
||||
|
||||
set_u128 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
|
||||
value := strconv.parse_u128(str) or_return
|
||||
if value > cast(u128)max(T) {
|
||||
return false
|
||||
}
|
||||
(^T)(ptr)^ = (T)(value)
|
||||
return true
|
||||
}
|
||||
|
||||
set_f64 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool {
|
||||
(^T)(ptr)^ = (T)(strconv.parse_f64(str) or_return)
|
||||
return true
|
||||
}
|
||||
|
||||
a := any{ptr, ti.id}
|
||||
|
||||
#partial switch t in ti.variant {
|
||||
case runtime.Type_Info_Dynamic_Array:
|
||||
ptr := (^runtime.Raw_Dynamic_Array)(ptr)
|
||||
|
||||
// Try to convert the value first.
|
||||
elem_backing, mem_err := mem.alloc_bytes(t.elem.size, t.elem.align)
|
||||
if mem_err != nil {
|
||||
return false
|
||||
}
|
||||
defer delete(elem_backing)
|
||||
parse_and_set_pointer_by_type(raw_data(elem_backing), value, t.elem) or_return
|
||||
|
||||
runtime.__dynamic_array_resize(ptr, t.elem.size, t.elem.align, ptr.len + 1) or_return
|
||||
subptr := cast(rawptr)(uintptr(ptr.data) + uintptr((ptr.len - 1) * t.elem.size))
|
||||
mem.copy(subptr, raw_data(elem_backing), len(elem_backing))
|
||||
|
||||
case runtime.Type_Info_Boolean:
|
||||
switch b in a {
|
||||
case bool: set_bool(ptr, bool, value) or_return
|
||||
case b8: set_bool(ptr, b8, value) or_return
|
||||
case b16: set_bool(ptr, b16, value) or_return
|
||||
case b32: set_bool(ptr, b32, value) or_return
|
||||
case b64: set_bool(ptr, b64, value) or_return
|
||||
}
|
||||
|
||||
case runtime.Type_Info_Rune:
|
||||
r := utf8.rune_at_pos(value, 0)
|
||||
if r == utf8.RUNE_ERROR { return false }
|
||||
(^rune)(ptr)^ = r
|
||||
|
||||
case runtime.Type_Info_String:
|
||||
switch s in a {
|
||||
case string: (^string)(ptr)^ = value
|
||||
case cstring: (^cstring)(ptr)^ = strings.clone_to_cstring(value)
|
||||
}
|
||||
case runtime.Type_Info_Integer:
|
||||
switch i in a {
|
||||
case int: set_i128(ptr, int, value) or_return
|
||||
case i8: set_i128(ptr, i8, value) or_return
|
||||
case i16: set_i128(ptr, i16, value) or_return
|
||||
case i32: set_i128(ptr, i32, value) or_return
|
||||
case i64: set_i128(ptr, i64, value) or_return
|
||||
case i128: set_i128(ptr, i128, value) or_return
|
||||
case i16le: set_i128(ptr, i16le, value) or_return
|
||||
case i32le: set_i128(ptr, i32le, value) or_return
|
||||
case i64le: set_i128(ptr, i64le, value) or_return
|
||||
case i128le: set_i128(ptr, i128le, value) or_return
|
||||
case i16be: set_i128(ptr, i16be, value) or_return
|
||||
case i32be: set_i128(ptr, i32be, value) or_return
|
||||
case i64be: set_i128(ptr, i64be, value) or_return
|
||||
case i128be: set_i128(ptr, i128be, value) or_return
|
||||
|
||||
case uint: set_u128(ptr, uint, value) or_return
|
||||
case uintptr: set_u128(ptr, uintptr, value) or_return
|
||||
case u8: set_u128(ptr, u8, value) or_return
|
||||
case u16: set_u128(ptr, u16, value) or_return
|
||||
case u32: set_u128(ptr, u32, value) or_return
|
||||
case u64: set_u128(ptr, u64, value) or_return
|
||||
case u128: set_u128(ptr, u128, value) or_return
|
||||
case u16le: set_u128(ptr, u16le, value) or_return
|
||||
case u32le: set_u128(ptr, u32le, value) or_return
|
||||
case u64le: set_u128(ptr, u64le, value) or_return
|
||||
case u128le: set_u128(ptr, u128le, value) or_return
|
||||
case u16be: set_u128(ptr, u16be, value) or_return
|
||||
case u32be: set_u128(ptr, u32be, value) or_return
|
||||
case u64be: set_u128(ptr, u64be, value) or_return
|
||||
case u128be: set_u128(ptr, u128be, value) or_return
|
||||
}
|
||||
case runtime.Type_Info_Float:
|
||||
switch f in a {
|
||||
case f16: set_f64(ptr, f16, value) or_return
|
||||
case f32: set_f64(ptr, f32, value) or_return
|
||||
case f64: set_f64(ptr, f64, value) or_return
|
||||
|
||||
case f16le: set_f64(ptr, f16le, value) or_return
|
||||
case f32le: set_f64(ptr, f32le, value) or_return
|
||||
case f64le: set_f64(ptr, f64le, value) or_return
|
||||
|
||||
case f16be: set_f64(ptr, f16be, value) or_return
|
||||
case f32be: set_f64(ptr, f32be, value) or_return
|
||||
case f64be: set_f64(ptr, f64be, value) or_return
|
||||
}
|
||||
case:
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@(private)
|
||||
get_struct_subtag :: proc(tag, id: string) -> (value: string, ok: bool) {
|
||||
tag := tag
|
||||
|
||||
for subtag in strings.split_iterator(&tag, ",") {
|
||||
if equals := strings.index_byte(subtag, '='); equals != -1 && id == subtag[:equals] {
|
||||
return subtag[1 + equals:], true
|
||||
} else if id == subtag {
|
||||
return "", true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
@(private)
|
||||
get_field_name :: proc(field: reflect.Struct_Field) -> string {
|
||||
if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_FLAGS); ok {
|
||||
if name_subtag, name_ok := get_struct_subtag(args_tag, SUBTAG_NAME); name_ok {
|
||||
return name_subtag
|
||||
}
|
||||
}
|
||||
|
||||
return field.name
|
||||
}
|
||||
|
||||
// Get a struct field by its field name or "name" subtag.
|
||||
// NOTE: `Error` uses the `context.temp_allocator` to give context about the error message
|
||||
get_field_by_name :: proc(data: ^$T, name: string) -> (field: reflect.Struct_Field, err: Error) {
|
||||
for field in reflect.struct_fields_zipped(T) {
|
||||
if get_field_name(field) == name {
|
||||
return field, nil
|
||||
}
|
||||
}
|
||||
|
||||
return {}, Parse_Error {
|
||||
.Missing_Field,
|
||||
fmt.tprintf("unable to find argument by name `%s`", name),
|
||||
}
|
||||
}
|
||||
|
||||
// Get a struct field by its "pos" subtag.
|
||||
get_field_by_pos :: proc(data: ^$T, index: int) -> (field: reflect.Struct_Field, ok: bool) {
|
||||
fields := reflect.struct_fields_zipped(T)
|
||||
|
||||
for field in fields {
|
||||
args_tag := reflect.struct_tag_lookup(field.tag, TAG_FLAGS) or_continue
|
||||
pos_subtag := get_struct_subtag(args_tag, SUBTAG_POS) or_continue
|
||||
value := strconv.parse_int(pos_subtag) or_continue
|
||||
if value == index {
|
||||
return field, true
|
||||
}
|
||||
}
|
||||
|
||||
return {}, false
|
||||
}
|
||||
45
core/flags/validation.odin
Normal file
45
core/flags/validation.odin
Normal file
@@ -0,0 +1,45 @@
|
||||
package flags
|
||||
|
||||
import "core:fmt"
|
||||
import "core:reflect"
|
||||
import "core:strconv"
|
||||
|
||||
_ :: fmt
|
||||
_ :: reflect
|
||||
_ :: strconv
|
||||
|
||||
// Validate that all the required arguments are set.
|
||||
validate :: proc(data: ^$T, max_pos: int, set_args: []string) -> Error {
|
||||
fields := reflect.struct_fields_zipped(T)
|
||||
|
||||
check_fields: for field in fields {
|
||||
tag := reflect.struct_tag_lookup(field.tag, TAG_ARGS) or_continue
|
||||
if _, ok := get_struct_subtag(tag, SUBTAG_REQUIRED); ok {
|
||||
was_set := false
|
||||
|
||||
// Check if it was set by name.
|
||||
check_set_args: for set_arg in set_args {
|
||||
if get_field_name(field) == set_arg {
|
||||
was_set = true
|
||||
break check_set_args
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it was set by position.
|
||||
if pos, has_pos := get_struct_subtag(tag, SUBTAG_POS); has_pos {
|
||||
value, value_ok := strconv.parse_int(pos)
|
||||
if value < max_pos {
|
||||
was_set = true
|
||||
}
|
||||
}
|
||||
|
||||
if !was_set {
|
||||
return Validation_Error {
|
||||
fmt.tprintf("required argument `%s` was not set", field.name),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user