Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539package slag
import ( "bytes" "errors" "fmt" "reflect" "strconv"
"github.com/danielgtaylor/casing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw")
var ( ErrNoImplemented = fmt.Errorf("to implement") ErrNoPointer = fmt.Errorf("structure provided is not a pointer") ErrNoStructUnderneath = fmt.Errorf("input provided is not a pointer to a struct") ErrNoFlagsProvided = fmt.Errorf("expected at least one flag name but got none") ErrNoFlagsValuesProvided = fmt.Errorf("expected value for this flag") ErrInvalidFlagName = fmt.Errorf("invalid flag name") ErrInvalidPropName = fmt.Errorf("invalid prop name") ErrTypeNotImplemented = fmt.Errorf("this type has not been implemented for parsing") separator = " " helpMessageTag = "slag_help")
func use(v ...any) {}
func SetHelpMessageTag(tag string) { helpMessageTag = tag}
func GetHelpMessageTag() string { return helpMessageTag}
func SetArgsSeparator(s string) { separator = s}
func errFlagValueIsMissing(flagName string) error { return fmt.Errorf("%v value is missing", flagName)}
func errTypeNotImplemented(t reflect.Kind) error { return fmt.Errorf("(%v) type has not been implemented for parsing", t)}
func isFlag(str string) bool { return str[0] == '-'}
func isProp(str string) bool { return str[0] == '.'}
// @Todo this two functions do the same thing, i should remove them and just handle the error// in placefunc parseFlagName(str string) (string, error) { if len(str) == 1 { return "", ErrInvalidFlagName } return str[1:], nil}
func parsePropName(str string) (string, error) { if len(str) == 1 { return "", ErrInvalidPropName } return str[1:], nil}
// returns array []string// returns obj// returns array []objfunc consumeFlagSimpleValues(args []string, pivot int) ([]string, int) { var i int var values []string
for i = pivot; i < len(args); i += 1 { a := args[i] if isFlag(a) { break } values = append(values, a) }
return values, i}
const _END_OBJ = "."
// @Todo: make this less error prone? it works well i guessfunc consumeFlagObjectValues(args []string, pivot int) (map[string]any, int) { var i int // @Todo: handle the i+=2 better // var step int // i+= step, the default value for step would be 2 when is a raw value, but N when an object/array was consumed values := make(map[string]any)
for i = pivot; i < len(args); { p := args[i] if isFlag(p) { break } // @Note we use a SLAG_END_OBJ to denotate an object has ended, can cause a bunch of bugs sadly if p == _END_OBJ { i += 1 break } p, err := parsePropName(args[i]) if err != nil { // @Todo: handle this error // kinda spaguetti i += 1 continue }
v := args[i+1] if isProp(v) { values[p], i = consumeFlagObjectValues(args, i+1) } else { values[p] = v i += 2 }
}
if len(values) == 0 { return nil, i }
return values, i}
type ArgKind = byte
const ( FlagKind ArgKind = '-' PropertyKind ArgKind = '.')
func isNextToken(args []string, pivot int, kind ArgKind) bool { if pivot >= len(args) { return false } return args[pivot][0] == kind}
type ArgValue any
func parseArgs(args []string) (map[string]any, error) { flagsAndValues := make(map[string]any) for i := 0; i < len(args); { var values any var name = args[i] pivot := i + 1 isFlag := isFlag(name)
if !isFlag { i = pivot continue }
flagName, err := parseFlagName(name)
if err != nil { return flagsAndValues, err }
// @Todo: handle object arrays // "-person .name mar .age 23 -hobbies reading" // "-people [.name marg .age 23, .name]" if isNextToken(args, pivot, PropertyKind) { var tmp map[string]any tmp, i = consumeFlagObjectValues(args, pivot) if tmp != nil { values = tmp } } else { var tmp []string tmp, i = consumeFlagSimpleValues(args, pivot) if tmp != nil { values = tmp } }
flagsAndValues[flagName] = values }
if len(flagsAndValues) == 0 { return flagsAndValues, ErrNoFlagsProvided }
return flagsAndValues, nil}
func unmarshalString(field reflect.Value, rawValues ...string) (err error) { kind := field.Kind() t := field.Type()
if kind == reflect.Bool && len(rawValues) == 0 { field.SetBool(true) return nil }
if len(rawValues) == 0 { return ErrNoFlagsValuesProvided }
first := rawValues[0]
switch kind { case reflect.Bool: b, err := strconv.ParseBool(first) if err != nil { return err } field.SetBool(b) case reflect.String: field.SetString(first) case reflect.Int: var number int64 number, err = strconv.ParseInt(first, 10, 64) if err != nil { return err } field.SetInt(number) case reflect.Float32: var decimal float64 decimal, err = strconv.ParseFloat(first, 32) if err != nil { return err } field.SetFloat(decimal) case reflect.Float64: var decimal float64 decimal, err = strconv.ParseFloat(first, 64) if err != nil { return err } field.SetFloat(decimal) case reflect.Slice: cap := field.Cap()
if cap > 0 { field.SetLen(cap) }
var i int var rv any
// We set the len to be the capacity // and when we stop iterating over the array // we just use i to set the real length // the reason on why i guess they do this (json/v2/arshal_default.go) would be to // not be calling SetLen in each of the iterations // as i was doing before, that's clever, it just makes me wonder how does the // appending work under the hood in golang cause when growing the array // we do not allocate just the memory we asked for, even if we are growing the capacity // i know this because it literally is what happens when calling grow it allocates and padds // so it gets powers of 2 aligned example 27 real elements makes the allocation be for -> 32 // also this doesn't cause any problems with the assigning because we are using accesing the fields and // assigning them values with the index, and we are iterating with the rawValues, which have the real quantity // so even if the array has an incorrect length for a little bit it doesn't introduce a bug cause again, this array/slice // will just be used when we finish up the filling of it for i, rv = range rawValues { if i == cap { field.Grow(1) cap = field.Cap() field.SetLen(cap) } idxField := field.Index(i) err := unmarshal(idxField, rv) if err != nil { // We need to set the len to be i because the current element // could not be appended to the array so that's why we need to // remove it from the length count (reason on why there is no + 1 here) field.SetLen(i) return err } } if i == 0 { emptySlice := reflect.MakeSlice(t, 0, 0) field.Set(emptySlice) } else { // Golang range loops leave the index variable always at the last element of the ranging array field.SetLen(i + 1) }
default: err = errTypeNotImplemented(kind) }
return}
// @Todo: add support for multiple objects in rawValuesfunc unmarshalObject(field reflect.Value, rawValues map[string]any) (err error) { // kind := field.Kind() // t := field.Type()
// @Todo: check if this ever is true if len(rawValues) == 0 { return ErrNoFlagsValuesProvided }
for name, value := range rawValues { fieldName := casing.Camel(name) f := field.FieldByName(fieldName) err = unmarshal(f, value) }
return err}
func unmarshal(field reflect.Value, value any) (err error) {
switch v := value.(type) { case string: err = unmarshalString(field, v) case []string: err = unmarshalString(field, v...) case map[string]any: err = unmarshalObject(field, v) }
return}
func Unmarshal(structure any, args map[string]any) error { tmp := reflect.ValueOf(structure) kind := tmp.Kind()
if kind != reflect.Pointer { return ErrNoPointer }
raw := tmp.Elem()
for name, value := range args { // @Update: casing module and check if they corrected the warning // this uses a deprecated thing // strings.Title is deprecated: The rule Title uses for word boundaries does not handle Unicode // punctuation properly. Use golang.org/x/text/cases instead. [default] fieldName := casing.Camel(name) field := raw.FieldByName(fieldName)
if !field.IsValid() { continue }
err := unmarshal(field, value) if err != nil { return err } }
return nil}
type HelpMessage struct { FlagName string Message string DefaultValue any Inner []HelpMessage}
func unwrap(v reflect.Value) reflect.Value { if v.Kind() == reflect.Pointer { v = v.Elem() } return v}
// We accept the type of the object we are typing because if one of the fields// is a pointer we can just use the type for accessing all the fields and struct info// which before that was being done with the value (that lead to problems because)// if the values are empty there is no such infofunc getStructFieldHelpMessage(v reflect.Type, d reflect.Value) []HelpMessage { var msgs []HelpMessage
if v.Kind() == reflect.Pointer { v = v.Elem() }
if d.Kind() == reflect.Pointer { d = d.Elem() }
for field := range v.Fields() { kind := field.Type.Kind()
if kind == reflect.Pointer { kind = field.Type.Elem().Kind() }
flagName := casing.Kebab(field.Name) message := field.Tag.Get("help") var helpMessage HelpMessage
switch kind { case reflect.Struct: innerStructDefaults := d.FieldByName(field.Name) innerHelpMessages := getStructFieldHelpMessage(field.Type, innerStructDefaults) helpMessage = HelpMessage{ FlagName: flagName, Inner: innerHelpMessages, } default: dflt := unwrap(d.FieldByName(field.Name)).Interface()
helpMessage = HelpMessage{ Message: message, FlagName: flagName, DefaultValue: dflt, } } msgs = append(msgs, helpMessage) } return msgs}
func GetHelpMessage(structure any, defaults any, helpTagName string) (string, error) { rv := reflect.ValueOf(structure) dv := reflect.ValueOf(defaults) if dv.Kind() != reflect.Struct || rv.Kind() != reflect.Struct { return "", ErrNoStructUnderneath }
var data [][]any
for f := range rv.Fields() { // @Todo: find another way to check if the user provided them cause // i kind wanted the user to be able to use whatever casing they want for the flags // but is not a priority tbh helpMsg, ok := f.Tag.Lookup(helpTagName) d := dv.FieldByName(f.Name)
fieldName := casing.Kebab(f.Name) if ok { flag := fmt.Sprintf("-%v", fieldName) data = append(data, []any{flag, helpMsg, d.Interface()}) }
}
symbols := tw.NewSymbolCustom("Help"). WithRow(" "). WithColumn(" ").WithCenter(" ")
b := []byte{} buf := bytes.NewBuffer(b) table := tablewriter.NewTable(buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{Symbols: symbols}))) table.Header("Flag", "Usage", "Default value") table.Bulk(data) table.Render()
completeHelpMsg := buf.String()
return completeHelpMsg, nil
}
type Config struct { ErrorOnUnkownFlags bool}
var DefaultConfig = Config{ErrorOnUnkownFlags: true}
// @Todo: refactor the unmarshal part using the previous funcfunc Parse(structure, defaults any, rawArgs []string, config Config) error { args, err := parseArgs(rawArgs) if err != nil && !errors.Is(err, ErrNoFlagsProvided) { return err }
tmp := reflect.ValueOf(structure) kind := tmp.Kind()
if kind != reflect.Pointer { return ErrNoPointer }
out := tmp.Elem()
if out.Kind() != reflect.Struct { return ErrNoStructUnderneath }
rDefaults := reflect.ValueOf(defaults)
// rDefaults := reflect.ValueOf(defaults)
for name, value := range args { // @Update: casing module and check if they corrected the warning // this uses a deprecated thing // strings.Title is deprecated: The rule Title uses for word boundaries does not handle Unicode // punctuation properly. Use golang.org/x/text/cases instead. [default] fieldName := casing.Camel(name) field := out.FieldByName(fieldName)
if !field.IsValid() { if config.ErrorOnUnkownFlags { // @Todo: // fuzzy find other flags with similar names to tell the user // did you mean "__name_of_similar_flag__"? return fmt.Errorf("%v is not a valid flag", name) } else { continue } }
// @Todo: remove this type assertion err := unmarshal(field, value) if err != nil { return err } }
for f, v := range out.Fields() { // @Todo: find another way to check if the user provided them cause // i kind wanted the user to be able to use whatever casing they want for the flags // but is not a priority tbh fieldName := casing.Kebab(f.Name) _, isProvided := args[fieldName] if isProvided { continue } dv := rDefaults.FieldByName(f.Name) v.Set(dv) }
return nil}