package main import ( "fmt" "strings" ) // Flag describes one supported command-line flag. type Flag struct { Name string Short string HasValue bool Default string Help string } // Parser stores flag definitions and parsed results. type Parser struct { flags map[string]Flag shortName map[string]string values map[string]string positionals []string } // NewParser creates a parser from the supplied flag definitions. func NewParser(flags []Flag) *Parser { p := &Parser{ flags: map[string]Flag{}, shortName: map[string]string{}, values: map[string]string{}, } for _, f := range flags { p.flags[f.Name] = f if f.Short != "" { p.shortName[f.Short] = f.Name } if f.Default != "" { p.values[f.Name] = f.Default } } return p } // Parse consumes argv-style arguments, excluding the program name. func (p *Parser) Parse(args []string) error { for i := 0; i < len(args); i++ { arg := args[i] if arg == "--" { p.positionals = append(p.positionals, args[i+1:]...) return nil } if strings.HasPrefix(arg, "--") { name, value, hasInlineValue := strings.Cut(strings.TrimPrefix(arg, "--"), "=") if err := p.setFlag(name, value, hasInlineValue, args, &i); err != nil { return err } continue } if strings.HasPrefix(arg, "-") && len(arg) > 1 { short := strings.TrimPrefix(arg, "-") name, ok := p.shortName[short] if !ok { return fmt.Errorf("unknown flag: -%s", short) } if err := p.setFlag(name, "", false, args, &i); err != nil { return err } continue } p.positionals = append(p.positionals, arg) } return nil } func (p *Parser) setFlag(name, value string, hasValue bool, args []string, index *int) error { flag, ok := p.flags[name] if !ok { return fmt.Errorf("unknown flag: --%s", name) } if !flag.HasValue { if hasValue { return fmt.Errorf("flag --%s does not accept a value", name) } p.values[name] = "true" return nil } if !hasValue { *index++ if *index >= len(args) { return fmt.Errorf("flag --%s requires a value", name) } value = args[*index] } p.values[name] = value return nil } // Get returns a parsed flag value, or an empty string when unset. func (p *Parser) Get(name string) string { return p.values[name] } // Bool reports whether a boolean flag was provided. func (p *Parser) Bool(name string) bool { return p.values[name] == "true" } // Positionals returns non-flag arguments. func (p *Parser) Positionals() []string { return append([]string(nil), p.positionals...) } // Help renders a compact usage summary. func (p *Parser) Help() string { var b strings.Builder for _, f := range p.flags { short := "" if f.Short != "" { short = fmt.Sprintf("-%s, ", f.Short) } value := "" if f.HasValue { value = " " } fmt.Fprintf(&b, " %s--%s%s\t%s\n", short, f.Name, value, f.Help) } return b.String() } func main() { parser := NewParser([]Flag{ {Name: "name", Short: "n", HasValue: true, Default: "world", Help: "name to greet"}, {Name: "verbose", Short: "v", Help: "enable verbose output"}, }) if err := parser.Parse([]string{"--name", "Ada", "-v", "input.txt"}); err != nil { panic(err) } if parser.Bool("verbose") { fmt.Println("verbose mode enabled") } fmt.Printf("hello, %s: %v\n", parser.Get("name"), parser.Positionals()) } // Tests, suitable for moving to parser_test.go in real projects. // // func TestParserLongFlagAndPositionals(t *testing.T) { // p := NewParser([]Flag{{Name: "output", Short: "o", HasValue: true}}) // if err := p.Parse([]string{"--output=app.bin", "main.go"}); err != nil { // t.Fatal(err) // } // if p.Get("output") != "app.bin" || p.Positionals()[0] != "main.go" { // t.Fatalf("unexpected parse result") // } // } // // func TestParserBoolAndMissingValue(t *testing.T) { // p := NewParser([]Flag{{Name: "verbose", Short: "v"}, {Name: "config", HasValue: true}}) // if err := p.Parse([]string{"-v"}); err != nil { // t.Fatal(err) // } // if !p.Bool("verbose") { // t.Fatalf("expected verbose flag") // } // if err := p.Parse([]string{"--config"}); err == nil { // t.Fatalf("expected missing value error") // } // }