package main import ( "fmt" "log" "os" "slices" "strings" ) // SetDollar0 returns []string{prog} when len(args) < 1, // else append([]string{prog}, args[1:]). // // Typical use (where "((prog))" is the literal, canonical program name): // func main() { // argmux := ... // defer argmux.Main(SetDollar0("((prog))", os.Args)) // // // ... // } func SetDollar0(prog string, args []string) []string { if len(args) > 0 { return slices.Clip(append([]string{prog}, slices.Clone(args[1:])...)) } return []string{prog} } // ArgMux multiplexes subcommands of a cli conglomerate. type ArgMux struct { Entries []Entry } // Main is guaranteed to call os.Exit(0) or os.Exit(1). func (argmux *ArgMux) Main(args []string) { err := func() error { for _, entry := range argmux.Entries { vars, ok := entry.Pattern.match(args) if !ok { continue } return entry.Action(vars) } return fmt.Errorf("unrecognized invocation: %q", args) }() if err != nil { log.Fatal(err) // BUG(jfrech): 2024-05-21: Ugly cli-bubbling error messages. os.Exit(1) return } os.Exit(0) return } // Add adds a pattern hook. // // Add may shadow patterns which are added after it. Add may panic. func (argmux *ArgMux) Add(pattern string, action func(vars *Vars) error) { seen := make(map[string]bool) see := func(name string) { if _, ok := seen[name]; ok { panic(fmt.Sprintf("duplicate variable: %q", name)) } seen[name] = true } var pats []Pat fields := strings.Fields(pattern) for j, field := range fields { switch { case len(field) == 0: panic("unreachable") case len(field) > 0 && field[0] == '$' && strings.HasSuffix(field, "..."): if j != len(fields)-1 { panic(fmt.Sprintf("non-final variadic variable: %q", field)) } var_ := field[1:len(field)-3] see(var_) pats = append(pats, Pat{IsVariable: true, IsVariadic: true, Name: var_}) case len(field) > 0 && field[0] == '$': var_ := field[1:] see(var_) pats = append(pats, Pat{IsVariable: true, Name: var_}) default: pats = append(pats, Pat{Name: field}) } } argmux.Entries = append(argmux.Entries, Entry{pats, action}) } // Entry hooks an action at a cli argument pattern. type Entry struct { Pattern Pattern Action func(vars *Vars) error } // Pattern is a cli argument pattern. type Pattern []Pat // Pat is either // a literal subcommand selector "brief view", // a variable "brief view $msgid" or // a variadic variable "briefi compose attach $filenames...". type Pat struct { IsVariable, IsVariadic bool Name string } // match matches a pattern against args. func (pattern Pattern) match(args []string) (*Vars, bool) { vars := &Vars{ _string: make(map[string]string), _strings: make(map[string][]string), } for j, pat := range pattern { if pat.IsVariable && pat.IsVariadic { if j != len(pattern)-1 { panic("unreachable") } vars._strings[pat.Name] = slices.Clone(args) return vars, true } if len(args) <= 0 { return nil, false } arg := args[0] args = args[1:] if pat.IsVariable { vars._string[pat.Name] = arg continue } else if pat.IsVariadic { panic("unreachable") } if pat.Name != arg { return nil, false } } if len(args) == 0 { return vars, true } return nil, false } // Vars holds matched variables. type Vars struct { _string map[string]string _strings map[string][]string } func (vars *Vars) String(name string) string { s, ok := vars._string[name] if !ok { panic(fmt.Sprintf("no $%q", name)) } return s } func (vars *Vars) Strings(name string) []string { ss, ok := vars._strings[name] if !ok { panic(fmt.Sprintf("no $%q...", name)) } return ss }