copied gophuwu flags into standalone repo
sophuwu sophie@sophuwu.com
Tue, 02 Sep 2025 22:09:02 +0200
A
flags.go
@@ -0,0 +1,256 @@
+package flaguwu + +import ( + "fmt" + "os" + "slices" +) + +type flag struct { + Name string + Short string + Type string + Help []string + Default interface{} + Value interface{} +} + +func (f flag) String() string { + var s string + if len(f.Short) == 1 { + s += fmt.Sprintf("-%s, ", f.Short) + } else { + s += fmt.Sprintf(" ") + } + s += fmt.Sprintf("--%s ", f.Name) + if f.Type == "bool" { + s += fmt.Sprintf("\n") + } else { + s += fmt.Sprintf("<%s>\n", f.Type) + } + if len(f.Help) > 0 { + s += fmt.Sprintf("\t%s ", f.Help[0]) + if f.Default != nil && !(f.Type == "string" && f.Default.(string) == "") { + s += fmt.Sprintf("(default: %v)\n", f.Default) + } else { + s += fmt.Sprintf("\n") + } + for _, v := range f.Help[1:] { + s += fmt.Sprintf("\t%s\n", v) + } + } + return s +} + +var FlagList = []flag{ + { + Name: "help", + Short: "h", + Help: []string{"Show this help message"}, + Type: "bool", + Default: false, + }, +} + +var FlagMap = map[string]*flag{ + "help": &(FlagList[0]), +} +var shortFlags = map[byte]string{ + 'h': "help", +} + +func AddHelp(flag string, helpMsg string) error { + f, ok := FlagMap[flag] + if !ok { + return fmt.Errorf("flag %s does not exist", flag) + } + f.Help = append(f.Help, helpMsg) + return nil +} + +func NewNewFlagWithHandler(handler func(err error)) func(name, short, helpMsg string, defaultValue interface{}) { + return func(name, short, helpMsg string, defaultValue interface{}) { + err := NewFlag(name, short, helpMsg, defaultValue) + if err != nil { + handler(err) + } + } +} + +// NewFlag creates a new flag with the given name, help message, and default value. +// It returns an error if the flag name is invalid, if the flag already exists, +// or if the default value is of an unsupported type. +// Supported types for defaultValue are: string, int, bool, and float64. +// short must be a single character and unique across all flags. +// set short to an empty string if no short flag is needed. +func NewFlag(name, short, helpMsg string, defaultValue interface{}) error { + if len(name) == 0 { + return fmt.Errorf("flag name cannot be empty") + } + if _, exists := FlagMap[name]; exists { + return fmt.Errorf("flag %s already exists", name) + } + if defaultValue == nil { + return fmt.Errorf("default value for flag %s cannot be nil", name) + } + if len(short) > 1 { + return fmt.Errorf("short flag must be a single character, or empty. %s is invalid", short) + } + var shrt *byte = nil + if len(short) == 1 { + b := short[0] + shrt = &b + if _, exists := shortFlags[b]; exists { + return fmt.Errorf("short flag %s already exists", short) + } + } + t := fmt.Sprintf("%T", defaultValue) + if !slices.Contains([]string{"string", "int", "bool", "float64"}, t) { + return fmt.Errorf("unsupported type %T in default value. Supported types are: string, int, bool, float64", defaultValue) + } + FlagList = append(FlagList, flag{ + Name: name, + Short: short, + Help: []string{helpMsg}, + Type: t, + Default: defaultValue, + }) + if shrt != nil { + shortFlags[*shrt] = name + } + FlagMap[name] = &FlagList[len(FlagList)-1] + return nil +} + +func getFlag(name, t string) (interface{}, error) { + f, exists := FlagMap[name] + if !exists { + return nil, fmt.Errorf("flag %s does not exist", name) + } + if f.Type != t { + return nil, fmt.Errorf("flag %s is not of type bool", name) + } + if f.Value == nil { + return f.Default, nil + } + return f.Value, nil +} +func GetBoolFlag(name string) (bool, error) { + i, err := getFlag(name, "bool") + if err != nil { + return false, err + } + return i.(bool), nil +} +func GetIntFlag(name string) (int, error) { + i, err := getFlag(name, "int") + if err != nil { + return 0, err + } + return i.(int), nil +} +func GetStringFlag(name string) (string, error) { + i, err := getFlag(name, "string") + if err != nil { + return "", err + } + return i.(string), nil +} + +func GetFloat64Flag(name string) (float64, error) { + i, err := getFlag(name, "float64") + if err != nil { + return 0, err + } + return i.(float64), nil +} + +var otherArgs []string + +func OtherArgs() []string { + return otherArgs +} + +func ParseArgs() error { + if len(os.Args) < 2 { + return nil + } + var v string + var vv byte + var i, j int + var f *flag + var ok bool + var err error + + shortFlags['h'] = "help" + + var args []string + for i = 1; i < len(os.Args); i++ { + v = os.Args[i] + if (len(v) > 2 && v[0] == '-' && v[1] != '-') || (len(v) == 2 && v[0] == '-') { + for j = 1; j < len(os.Args[i]); j++ { + vv = os.Args[i][j] + v, ok = shortFlags[vv] + if !ok { + return fmt.Errorf("unknown short flag: %c", vv) + } + args = append(args, "--"+v) + } + continue + } + args = append(args, v) + } + + for i = 0; i < len(args); i++ { + v = args[i] + if len(v) > 2 && v[0] == '-' && v[1] == '-' { + v = v[2:] + f, ok = FlagMap[v] + if !ok { + return fmt.Errorf("unknown flag: %s", v) + } + if f.Type == "bool" { + f.Value = !f.Default.(bool) + continue + } + i++ + v = args[i] + if i >= len(args) { + return fmt.Errorf("flag %s requires a value", v) + } + switch f.Type { + case "string": + f.Value = v + break + case "int": + f.Value, err = parseInt(v) + break + case "float64": + f.Value, err = parseFloat(v) + break + default: + return fmt.Errorf("unsupported flag type for %s", f.Name) + } + if err != nil { + return fmt.Errorf("error parsing flag %s: %v", f.Name, err) + } + } else { + otherArgs = append(otherArgs, args[i]) + } + } + if ok, err = GetBoolFlag("help"); err != nil { + return fmt.Errorf("error getting help flag: %v", err) + } else if ok { + PrintHelp() + os.Exit(0) + } + return nil +} + +func PrintHelp() { + fmt.Printf("Usage: %s [options]\n", os.Args[0]) + fmt.Println("Options:") + for _, f := range FlagList { + fmt.Print(f.String()) + } +}
A
main.go
A
parse.go
@@ -0,0 +1,113 @@
+package flaguwu + +import ( + "errors" + "io" + "runtime" +) + +func parseInt(s string) (int, error) { + n := 0 + if len(s) == 0 { + return 0, errors.New("empty string cannot be parsed to int") + } + sign := 1 + if s[0] == '-' { + sign = -1 + s = s[1:] + } + for _, r := range s { + if r < '0' || r > '9' { + return n * sign, errors.New("invalid character in string, must be a digit") + } + n = n*10 + int(r-'0') + } + n *= sign + return n, nil +} + +func parseFloat(s string) (float64, error) { + if len(s) == 0 { + return 0, errors.New("empty string cannot be parsed to float") + } + n := 0.0 + sign := 1.0 + if s[0] == '-' { + sign = -1.0 + s = s[1:] + } + decimalPoint := false + factor := 1.0 + for _, r := range s { + if r == '.' { + if decimalPoint { + return n * sign, errors.New("multiple decimal points in string") + } + decimalPoint = true + continue + } + if r < '0' || r > '9' { + return n * sign, errors.New("invalid character in string, must be a digit or decimal point") + } + if decimalPoint { + factor *= 10.0 + n += float64(r-'0') / factor + } else { + n = n*10 + float64(r-'0') + } + } + n *= sign + return n, nil +} + +func parseBool(s string) (bool, error) { + switch s { + case "true", "1", "yes", "on": + return true, nil + case "false", "0", "no", "off": + return false, nil + default: + return false, errors.New("invalid boolean string, must be true/false, 1/0, yes/no, on/off") + } +} + +func readLine(reader io.Reader) ([]byte, error) { + var buf [1]byte + var ret []byte + + for { + n, err := reader.Read(buf[:]) + if n > 0 { + switch buf[0] { + case '\b': + if len(ret) > 0 { + ret = ret[:len(ret)-1] + } + case '\n': + if runtime.GOOS != "windows" { + return ret, nil + } + // otherwise ignore \n + case '\r': + if runtime.GOOS == "windows" { + return ret, nil + } + // otherwise ignore \r + default: + ret = append(ret, buf[0]) + } + continue + } + if err != nil { + if err == io.EOF && len(ret) > 0 { + return ret, nil + } + return ret, err + } + } +} + +func readLineString(reader io.Reader) (string, error) { + line, err := readLine(reader) + return string(line), err +}