package cli import ( "errors" "fmt" "github.com/mattn/go-runewidth" "strconv" "strings" ) var ( ErrNotFound = errors.New("not found") ) type Router struct { name string path []string children []*Router command Command params []string } func (r *Router) getChildren(name string) *Router { for _, child := range r.children { if child.name == name { return child } } return nil } func (r *Router) Completer(tokens ...string) []string { ss := make([]string, 0, 10) if len(tokens) == 0 { for _, child := range r.children { ss = append(ss, strings.Join(child.path, " ")) } return ss } children := r.getChildren(tokens[0]) if children == nil { token := tokens[0] for _, child := range r.children { if strings.HasPrefix(child.name, token) { ss = append(ss, strings.Join(child.path, " ")) } } return ss } return children.Completer(tokens[1:]...) } func (r *Router) Usage() string { if len(r.path) <= 0 { return "" } var ( sb strings.Builder ) sb.WriteString("Usage: ") sb.WriteString(strings.Join(r.path, " ")) if len(r.params) > 0 { for _, s := range r.params { sb.WriteString(" {" + s + "}") } } return sb.String() } func (r *Router) Handle(path string, command Command) { var ( pos int name string ) if strings.HasSuffix(path, "/") { path = strings.TrimSuffix(path, "/") } if strings.HasPrefix(path, "/") { path = strings.TrimPrefix(path, "/") } if path == "" { r.command = command return } if path[0] == ':' { ss := strings.Split(path, "/") for _, s := range ss { r.params = append(r.params, strings.TrimPrefix(s, ":")) } r.command = command return } if pos = strings.IndexByte(path, '/'); pos > -1 { name = path[:pos] path = path[pos:] } else { name = path path = "" } if name == "-" { name = "app" } children := r.getChildren(name) if children == nil { children = newRouter(name) if len(r.path) == 0 { children.path = append(children.path, name) } else { children.path = append(children.path, r.path...) children.path = append(children.path, name) } r.children = append(r.children, children) } if children.command.Handle != nil { panic("a handle is already registered for path /" + strings.Join(children.path, "/")) } children.Handle(path, command) } func (r *Router) Lookup(tokens []string) (router *Router, args []string, err error) { if len(tokens) > 0 { children := r.getChildren(tokens[0]) if children != nil { return children.Lookup(tokens[1:]) } } if r.command.Handle == nil { err = ErrNotFound return } router = r args = tokens return } func (r *Router) String() string { var ( sb strings.Builder width int maxWidth int walkFunc func(router *Router) []commander ) walkFunc = func(router *Router) []commander { vs := make([]commander, 0, 5) if router.command.Handle != nil { vs = append(vs, commander{ Name: router.name, Path: strings.Join(router.path, " "), Description: router.command.Description, }) } else { if len(router.children) > 0 { for _, child := range router.children { vs = append(vs, walkFunc(child)...) } } } return vs } vs := walkFunc(r) for _, v := range vs { width = runewidth.StringWidth(v.Path) if width > maxWidth { maxWidth = width } } for _, v := range vs { sb.WriteString(fmt.Sprintf("%-"+strconv.Itoa(maxWidth+4)+"s %s\n", v.Path, v.Description)) } return sb.String() } func newRouter(name string) *Router { return &Router{ name: name, path: make([]string, 0, 4), params: make([]string, 0, 4), children: make([]*Router, 0, 10), } }