Compare commits

...
13 Commits
4 changed files with 176 additions and 34 deletions
+19
View File
@@ -0,0 +1,19 @@
# zlib License
(C) 2026 Simon Kellet
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
+34 -2
View File
@@ -19,8 +19,14 @@ This a very simple implementation of Go-like flags in Odin
fleg.parse_flags()
```
You can also marks flags as **required**. These will still be parsed, however will hault the application until they are passed:
***Supports: bool, int, string, f32***
```odin
import "fleg"
isRunning: bool
fleg.BoolVar(&isRunning, "isRunning", true, "A select piece of code is running!", required = true)
fleg.parse_flags()
```
And on the command line, you pass flags like so:
@@ -28,6 +34,8 @@ And on the command line, you pass flags like so:
./<program name> -isNumber=45 -isRunning=false ......
```
***Supports: bool, int, string, f32 and f64***
## Help flag:
The package also contains a help context menu. This comes default and can be called like so (**Note:** You can also use "-h" or "--help"):
@@ -54,7 +62,20 @@ The package also contains a help context menu. This comes default and can be cal
## Custom Flag Format
A custom flag format can be created by overwriting the strings *FLAG_START_CHAR* and *FLAG_SEP_CHAR*. These cannot be the same however!
A custom flag format can be created by overwriting the strings *FLAG_START_CHAR* and *FLAG_SEP_CHAR*. (These *cannot* be the same however!)
```odin
fleg.FLAG_START_CHAR = "--"
fleg.FLAG_SEP_CHAR = ":"
```
Flags will now be in the format:
```txt
--isString:"......"
```
**Note:** Any change to the format of the flags is also relfected upon the "help" flag.
## Custom Allocator
@@ -70,3 +91,14 @@ If needed, you are able to setup a custom allocator like so:
fleg.init_custom_allocator(arena_alloc)
defer fleg.destroy() //clean up flags
```
### Mirror
This repo is being developed on Gitea [here](https://git.simonkellet.xyz/simonkellet/odin-flags). The Github ([here](https://github.com/simonkellet404/odin-flags)) is a mirror.
## TODO
* More type support
* Better logging of INFO and ERROR's
* String validation (perhaps not needed?)
* Array support (e.g. **--isArray:{4,5,6,7}**)
+4 -2
View File
@@ -1,6 +1,7 @@
package main
import "core:fmt"
import vmem "core:mem/virtual"
import "fleg"
main :: proc(){
@@ -12,6 +13,7 @@ main :: proc(){
defer vmem.arena_destroy(&arena) //clean
fleg.init_custom_allocator(arena_alloc)
defer fleg.destroy()
*/
fleg.FLAG_START_CHAR = ":"
@@ -24,8 +26,8 @@ main :: proc(){
isString: string
isFloat: f32
fleg.BoolVar(&isRunning, "isRunning", true, "A select piece of code is running!")
fleg.IntVar(&isNumber, "isNumber", 3434, "A number!")
fleg.BoolVar(&isRunning, "isRunning", false, "A select piece of code is running!", required = true)
fleg.IntVar(&isNumber, "isNumber", 3434, "A number!", required = true)
fleg.StringVar(&isString, "isString", "Hellope!", "A string!")
fleg.Float32Var(&isFloat, "isFloat", 3.1415, "A float!")
fleg.parse_flags()
+117 -28
View File
@@ -33,25 +33,32 @@ FORCE_HELP_ON_EMPTY_ARGS := false
FLAG_START_CHAR := "-"
FLAG_SEP_CHAR := "="
@(private)
Flag_Value_Ptr :: union {
^bool,
^int,
^i32,
^i64,
^string,
^f32,
^f64,
}
@(private)
Flag :: struct {
name: string,
value: Flag_Value_Ptr,
usage: string,
parsed: bool, // sck: flag to check if it has been parsed
required: bool, // sck: required flag
}
// Global dynamic array of Flags
@(private)
all_flags: [dynamic]Flag
// runtime allocator for the flags
@(private)
flag_allocator: runtime.Allocator
// sck: Used to create a customer allocator
@@ -63,84 +70,136 @@ init_custom_allocator :: proc(allocator: runtime.Allocator) {
}
destroy :: proc() {
free_all(flag_allocator)
delete(all_flags)
all_flags = {} // sck: needed?
all_flags = {}
}
@(private)
print_flags :: proc() {
for f in all_flags {
fmt.printfln("%s: %v: %s", f.name, f.value, f.usage)
fmt.printfln("%s: %s (required=%v)", f.name, f.usage, f.required)
}
}
@(private)
print_flag_format :: proc(){
fmt.printfln("Want %s<flag>%s<value>\n", FLAG_START_CHAR, FLAG_SEP_CHAR)
}
@(private)
print_usage :: proc() {
fmt.println("\tUsage: ")
fmt.println("\n\tFlag format for this program:")
fmt.printfln("\t%s<flag>%s<value>\n", FLAG_START_CHAR, FLAG_SEP_CHAR)
for f in all_flags {
req_msg: string
if f.required { req_msg = " (required)"}
switch v in f.value {
case ^bool:
fmt.printfln("\t%s%s:\n\t\t%s (default: %v)\n", FLAG_START_CHAR, f.name, f.usage, v^)
fmt.printfln("\t%s%s:\n\t\t%s (default: %v)%s\n", FLAG_START_CHAR, f.name, f.usage, v^, req_msg)
case ^int:
fmt.printfln("\t%s%s:\n\t\t%s (default: %d)\n", FLAG_START_CHAR, f.name, f.usage, v^)
fmt.printfln("\t%s%s:\n\t\t%s (default: %d)%s\n", FLAG_START_CHAR, f.name, f.usage, v^, req_msg)
case ^i32:
fmt.printfln("\t%s%s:\n\t\t%s (default: %d)%s\n", FLAG_START_CHAR, f.name, f.usage, v^, req_msg)
case ^i64:
fmt.printfln("\t%s%s:\n\t\t%s (default: %d)%s\n", FLAG_START_CHAR, f.name, f.usage, v^, req_msg)
case ^string:
fmt.printfln("\t%s%s:\n\t\t%s (default: %s)\n", FLAG_START_CHAR, f.name, f.usage, v^)
fmt.printfln("\t%s%s:\n\t\t%s (default: %s)%s\n", FLAG_START_CHAR, f.name, f.usage, v^, req_msg)
case ^f32:
fmt.printfln("\t%s%s:\n\t\t%s (default: %f)\n", FLAG_START_CHAR, f.name, f.usage, v^)
fmt.printfln("\t%s%s:\n\t\t%s (default: %f)%s\n", FLAG_START_CHAR, f.name, f.usage, v^, req_msg)
case ^f64:
fmt.printfln("\t%s%s:\n\t\t%s (default: %f)\n", FLAG_START_CHAR, f.name, f.usage, v^)
fmt.printfln("\t%s%s:\n\t\t%s (default: %f)%s\n", FLAG_START_CHAR, f.name, f.usage, v^, req_msg)
}
}
os.exit(0)
}
BoolVar :: proc(ptr: ^bool, name: string, default: bool, usage: string) {
BoolVar :: proc(ptr: ^bool, name: string, default: bool, usage: string, required := false) {
if ptr == nil {
fmt.fprintfln(os.stderr, "[ERROR]: Invalid usage of BoolVar: got nil for 'ptr' value!")
os.exit(1)
}
ptr^ = default
append(&all_flags, Flag{name = name, value = ptr, usage = usage})
append(&all_flags, Flag{name = name, value = ptr, usage = usage, required = required})
}
IntVar :: proc(ptr: ^int, name: string, default: int, usage: string) {
IntVar :: proc(ptr: ^int, name: string, default: int, usage: string, required := false) {
if ptr == nil {
fmt.fprintfln(os.stderr, "[ERROR]: Invalid usage of IntVar: got nil for 'ptr' value!")
os.exit(1)
}
ptr^ = default
append(&all_flags, Flag{name = name, value = ptr, usage = usage})
append(&all_flags, Flag{name = name, value = ptr, usage = usage, required = required})
}
StringVar :: proc(ptr: ^string, name: string, default: string, usage: string) {
Int32Var :: proc(ptr: ^i32, name: string, default: i32, usage: string, required := false) {
if ptr == nil {
fmt.fprintfln(os.stderr, "[ERROR]: Invalid usage of Int32Var: got nil for 'ptr' value!")
os.exit(1)
}
ptr^ = default
append(&all_flags, Flag{name = name, value = ptr, usage = usage})
append(&all_flags, Flag{name = name, value = ptr, usage = usage, required = required})
}
Float32Var :: proc(ptr: ^f32, name: string, default: f32, usage: string) {
Int64Var :: proc(ptr: ^i64, name: string, default: i64, usage: string, required := false) {
if ptr == nil {
fmt.fprintfln(os.stderr, "[ERROR]: Invalid usage of Int32Var: got nil for 'ptr' value!")
os.exit(1)
}
ptr^ = default
append(&all_flags, Flag{name = name, value = ptr, usage = usage})
append(&all_flags, Flag{name = name, value = ptr, usage = usage, required = required})
}
Float64Var :: proc(ptr: ^f64, name: string, default: f64, usage: string) {
StringVar :: proc(ptr: ^string, name: string, default: string, usage: string, required := false) {
if ptr == nil {
fmt.fprintfln(os.stderr, "[ERROR]: Invalid usage of StringVar: got nil for 'ptr' value!")
os.exit(1)
}
ptr^ = default
append(&all_flags, Flag{name = name, value = ptr, usage = usage})
append(&all_flags, Flag{name = name, value = ptr, usage = usage, required = required})
}
Float32Var :: proc(ptr: ^f32, name: string, default: f32, usage: string, required := false) {
if ptr == nil {
fmt.fprintfln(os.stderr, "[ERROR]: Invalid usage of Float32Var: got nil for 'ptr' value!")
os.exit(1)
}
ptr^ = default
append(&all_flags, Flag{name = name, value = ptr, usage = usage, required = required})
}
Float64Var :: proc(ptr: ^f64, name: string, default: f64, usage: string, required := false) {
if ptr == nil {
fmt.fprintfln(os.stderr, "[ERROR]: Invalid usage of Float64Var: got nil for 'ptr' value!")
os.exit(1)
}
ptr^ = default
append(&all_flags, Flag{name = name, value = ptr, usage = usage, required = required})
}
parse_flags :: proc() {
// sck: We cannot have the start and seperator formats being the same
// ...leads to parsing errors...
if FLAG_START_CHAR == FLAG_SEP_CHAR {
fmt.println("[ERROR]: FLAG_START_CHAR and FLAG_SEP_CHAR cannot be the same!")
fmt.fprintfln(os.stderr, "[ERROR]: FLAG_START_CHAR and FLAG_SEP_CHAR cannot be the same!")
fmt.printfln("\t FLAG_START_CHAR= \"%s\"\t FLAG_SEP_CHAR= \"%s\"", FLAG_START_CHAR, FLAG_SEP_CHAR)
os.exit(1)
}
if len(os.args) < 2 && FORCE_HELP_ON_EMPTY_ARGS {print_usage()}
for &a in os.args {
if a == "-h" || a == "-help" || a == "--help" {print_usage()}
// skip the first arg (1:)
for &a in os.args[1:] {
if a == "-h" || a == "-help" || a == "--help" {print_usage(); os.exit(0)}
// sck: if the user has a custom flag format, copy that to the help flag too.
if a == fmt.aprintf("%s%s", FLAG_START_CHAR, "help", allocator = flag_allocator) {print_usage()}
if a == fmt.aprintf("%s%s", FLAG_START_CHAR, "h", allocator = flag_allocator) {print_usage()}
if a == os.args[0] {continue}
if a == fmt.aprintf("%s%s", FLAG_START_CHAR, "help", allocator = context.temp_allocator) {print_usage(); os.exit(0)}
if a == fmt.aprintf("%s%s", FLAG_START_CHAR, "h", allocator = context.temp_allocator) {print_usage(); os.exit(0)}
defer free_all(context.temp_allocator) //clean up!
// Begin parsing flags...
for &f in all_flags {
if f.parsed {continue}
a = strings.trim_prefix(a, FLAG_START_CHAR)
a = strings.trim_prefix(a, FLAG_START_CHAR)
name := a
value: string
@@ -151,10 +210,11 @@ parse_flags :: proc() {
} else {
name = a
fmt.printf("[INFO]: Could not read flag: %s. ", name)
fmt.printfln("Want %s<flag>%s<value>\n", FLAG_START_CHAR, FLAG_SEP_CHAR)
print_flag_format()
break
}
// sck: We are going to parse all the flags
if name == f.name && !f.parsed {
f.parsed = true
switch &v in f.value {
@@ -178,6 +238,26 @@ parse_flags :: proc() {
}
v^ = parsed
case ^i32:
if value == "" {break}
parsed, ok := strconv.parse_int(value)
if !ok {
fmt.fprintfln(os.stderr, "[ERROR] Failed to parse flag %s: got %T, wanted int",
f.name, value)
os.exit(1)
}
v^ = cast(i32)parsed
case ^i64:
if value == "" {break}
parsed, ok := strconv.parse_i64(value)
if !ok {
fmt.fprintfln(os.stderr, "[ERROR] Failed to parse flag %s: got %T, wanted int",
f.name, value)
os.exit(1)
}
v^ = parsed
case ^string:
if value == "" {break}
// TODO: validate string?
@@ -206,4 +286,13 @@ parse_flags :: proc() {
}
}
}
for f in all_flags {
if f.required && !f.parsed {
// sck: INFO or ERROR?
fmt.fprintfln(os.stderr, "[INFO] Missing required flag: %s%s", FLAG_START_CHAR, f.name)
if !FORCE_HELP_ON_EMPTY_ARGS {fmt.fprintfln(os.stderr, "use -help or -h to view all flags")}
os.exit(1)
}
}
}