package main import ( "os" "sort" "strconv" "strings" "time" ) var configMap = map[string]configParameter{} var configAliasMap = map[string]string{} func init() { registerCommand("help", "show program usage", func() { log("", 0, "options:") parms := make([]string, 0, len(configMap)) for key := range configMap { parms = append(parms, key) } sort.Strings(parms) for _, parmName := range parms { parm := configMap[parmName] if parm.hidden { continue } header := "-" + parm.name if len(parm.name) > 1 { header = "-" + header } aliases := []string{} for alias, target := range configAliasMap { if target == parm.name { if len(alias) == 1 { aliases = append(aliases, "-"+alias) } else { aliases = append(aliases, "--"+alias) } } } if len(aliases) > 0 { sort.Strings(aliases) header = header + " (aliases: " + strings.Join(aliases, ", ") + ")" } header = header + ":" full := header if parm.description != "" { full = header + "\n " + parm.description } if parm.command || parm.sw { log("", 0, "%s", full) } else { log("", 0, "%s\n default: %v", full, parm.value) } } log("", 0, "") log("", 0, "examples:") log("", 0, " single target:") log("", 0, " ./mtbf --ip 127.0.0.1 --port 8291 --login admin --password 12345678 --out-file good.txt") log("", 0, " multiple targets with multiple passwords:") log("", 0, " ./mtbf --ip-list ips.txt --port 8291 --login admin --password-list passwords.txt --out-file good.txt") os.Exit(0) }) registerAlias("?", "help") registerAlias("h", "help") } // configParameterOptions represents additional options for a configParameter. type configParameterOptions struct { sw, hidden, command bool callback func() } // configParameter represents a single configuration parameter. type configParameter struct { name string // duplicated in configMap, but also saved here for convenience value, def any // value and default value description string // description for this parameter parsed bool // true if it was successfully parsed from commandline configParameterOptions } type configParameterTypeUnion = interface { bool | int | uint | float64 | string | []int | []uint | []float64 | []string | []bool | map[string]bool } // -------------- // parsing // -------------- func ParseConfig() { log("cfg", 1, "parsing config") totalFinalized := 0 for i := 1; i < len(os.Args); i++ { arg := getCmdlineParm(i) if len(arg) == 0 { continue } failIf(arg[0] != '-', "\"%v\" is not a commandline parameter", arg) arg = strings.TrimPrefix(arg, "-") arg = strings.TrimPrefix(arg, "-") failIf(len(arg) == 0, "\"%v\" is not a commandline parameter", getCmdlineParm(i)) parm, ok := configMap[strings.ToLower(arg)] if !ok { alias, ok := configAliasMap[strings.ToLower(arg)] failIf(!ok, "unknown commandline parameter: \"%v\"", arg) parm, ok = configMap[alias] failIf(!ok, "alias \"%v\" references unknown commandline parameter", arg) log("cfg", 3, "\"%v\" is aliased to \"%v\"", alias, parm.name) } failIf(parm.hidden, "\"%v\" is not a commandline parameter", getCmdlineParm(i)) failIf(parm.parsed && !parm.isSlice(), "multiple occurrences of commandline parameter \"%v\" are not allowed", parm.name) if !parm.command { if parm.sw { parm.writeParmValue("true") } else { i++ failIf(i >= len(os.Args), "missing value for \"%v\"", parm.name) parm.writeParmValue(getCmdlineParm(i)) } } parm.finalize() totalFinalized++ } log("cfg", 1, "ok: finished parsing config, got %v parameters", totalFinalized) } // getCmdlineParm retrieves a commandline parameter with index i. func getCmdlineParm(i int) string { return strings.TrimSpace(os.Args[i]) } // isSlice checks if a configParameter value is a slice. func (parm *configParameter) isSlice() bool { switch parm.value.(type) { case []int, []uint, []bool, []string: return true default: return false } } func (parm *configParameter) clearSlice() { if !parm.parsed { switch parm.value.(type) { case []int: parm.value = []int{} case []uint: parm.value = []uint{} case []bool: parm.value = []bool{} case []string: parm.value = []string{} } } } // writeParmValue saves raw commandline value into a configParameter. func (parm *configParameter) writeParmValue(value string) { var err error switch parm.value.(type) { case bool: parm.value, err = strconv.ParseBool(value) failIf(err != nil, "config parameter \"%v\" must be a boolean", parm.name) case int: v, err := strconv.ParseInt(value, 10, 0) failIf(err != nil, "config parameter \"%v\" must be an integer", parm.name) parm.value = int(v) case uint: v, err := strconv.ParseUint(value, 10, 0) failIf(err != nil, "config parameter \"%v\" must be an unsigned integer", parm.name) parm.value = uint(v) case string: parm.value = value case []bool: b, err := strconv.ParseBool(value) failIf(err != nil, "config parameter \"%v\" must be a boolean", parm.name) parm.clearSlice() parm.value = append(parm.value.([]bool), b) case []int: i, err := strconv.ParseInt(value, 10, 0) failIf(err != nil, "config parameter \"%v\" must be an integer", parm.name) parm.clearSlice() parm.value = append(parm.value.([]int), int(i)) case []uint: u, err := strconv.ParseUint(value, 10, 0) failIf(err != nil, "config parameter \"%v\" must be an unsigned integer", parm.name) parm.clearSlice() parm.value = append(parm.value.([]uint), uint(u)) case []string: parm.clearSlice() parm.value = append(parm.value.([]string), value) default: fail("unknown config parameter \"%v\" type: %T", parm.name, parm.value) } } // finalize marks a configParameter as parsed, adds it to a global config map // and calls its callback, if one is present. func (parm *configParameter) finalize() { parm.parsed = true configMap[parm.name] = *parm if parm.callback != nil { parm.callback() } log("cfg", 2, "parse: %T \"%v\" -> def %v, now %v", parm.value, parm.name, parm.def, parm.value) } // -------------- // registration // -------------- func registerConfigParameter[T configParameterTypeUnion](name string, def T, description string, opts configParameterOptions) { name = strings.ToLower(name) _, ok := configMap[name] failIf(ok, "cannot register config parameter (already exists): \"%v\"", name) _, ok = configAliasMap[name] failIf(ok, "cannot register config parameter (already exists as an alias): \"%v\"", name) failIf(opts.command && opts.callback == nil, "\"%v\" is defined as a command but callback is missing", name) configMap[name] = configParameter{name: name, value: def, def: def, description: description, configParameterOptions: opts} } func registerParam[T configParameterTypeUnion](name string, def T, description string) { registerConfigParameter(name, def, description, configParameterOptions{}) } func registerParamHidden[T configParameterTypeUnion](name string, def T) { registerConfigParameter(name, def, "", configParameterOptions{hidden: true}) } func registerParamWithCallback[T configParameterTypeUnion](name string, def T, description string, callback func()) { registerConfigParameter(name, def, description, configParameterOptions{callback: callback}) } func registerCommand(name string, description string, callback func()) { registerConfigParameter(name, false, description, configParameterOptions{command: true, callback: callback}) } func registerSwitch(name string, description string) { registerConfigParameter(name, false, description, configParameterOptions{sw: true}) } func registerAlias(alias, target string) { alias, target = strings.ToLower(alias), strings.ToLower(target) _, ok := configAliasMap[alias] failIf(ok, "cannot register alias (already exists): \"%v\"", alias) _, ok = configMap[alias] failIf(ok, "cannot register alias (already exists as a config parameter): \"%v\"", alias) _, ok = configMap[target] failIf(!ok, "cannot register alias \"%v\": target \"%v\" does not exist", alias, target) configAliasMap[alias] = target } // -------------- // acquisition // -------------- func getParamGeneric(name string) any { name = strings.ToLower(name) parm, ok := configMap[name] failIf(!ok, "unknown config parameter: \"%v\"", name) failIf(parm.command, "config parameter \"%v\" is a command", name) if parm.sw { return parm.parsed // switches always return true if they were parsed } else if parm.parsed || parm.hidden { return parm.value // parsed and hidden parms return their current value } else { return parm.def // otherwise, use default value } } func getParam[T configParameterTypeUnion](name string) T { return getParamGeneric(name).(T) } func getParamInt(name string) int { return getParam[int](name) } func getParamFloat(name string) float64 { return getParam[float64](name) } func getParamIntSlice(name string) []int { return getParam[[]int](name) } func getParamBool(name string) bool { return getParam[bool](name) } func getParamSwitch(name string) bool { return getParamBool(name) } func getParamString(name string) string { return getParam[string](name) } func getParamStringSlice(name string) []string { return getParam[[]string](name) } func getParamDurationMS(name string) time.Duration { tm := getParam[int](name) failIf(tm < -1, "\"%v\" can only be set to -1 or a positive value", name) if tm == -1 { tm = 0 } return time.Duration(tm) * time.Millisecond } // -------------- // setting // -------------- func setParam(name string, value any) { name = strings.ToLower(name) parm, ok := configMap[name] failIf(!ok, "unknown config parameter: \"%v\"", name) failIf(parm.command, "config parameter \"%v\" is a command and cannot be set", name) failIf(parm.sw && !value.(bool), "config parameter \"%v\" is a switch and only accepts boolean arguments", name) parm.value = value if parm.callback != nil { parm.callback() } configMap[name] = parm }