bonzai/cmd.go
2022-02-25 21:46:36 -05:00

229 lines
5.8 KiB
Go

// Copyright 2022 Robert S. Muhlestein.
// SPDX-License-Identifier: Apache-2.0
package bonzai
import (
"fmt"
"os"
"github.com/rwxrob/bonzai/comp"
"github.com/rwxrob/bonzai/loop"
)
// Cmd is a struct the easier to use and read when creating
// implementations of the Command interface.
//
// Params
//
// Params require a Method. While Methods may receive any number of
// arguments, Params are a way of helping completion for regular
// parameters. Standard completion will not recursively complete
// multiple params, one param per completion.
type Cmd struct {
Name string `json:"name,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Summary string `json:"summary,omitempty"`
Usage string `json:"usage,omitempty"`
Version string `json:"version,omitempty"`
Copyright string `json:"copyright,omitempty"`
License string `json:"license,omitempty"`
Description string `json:"description,omitempty"`
Site string `json:"site,omitempty"`
Source string `json:"source,omitempty"`
Issues string `json:"issues,omitempty"`
Other map[string]string `json:"issues,omitempty"`
Commands []*Cmd `json:"commands,omitempty"`
Params []string `json:"params,omitempty"`
Hidden []string `json:"hide,omitempty"`
Completer comp.Completer `json:"-"`
Call Method `json:"-"`
Caller *Cmd `json:"-"`
_aliases map[string]*Cmd
}
func (x *Cmd) cacheAliases() {
x._aliases = map[string]*Cmd{}
if x.Commands == nil {
return
}
for _, c := range x.Commands {
if c.Aliases == nil {
continue
}
for _, a := range c.Aliases {
x._aliases[a] = c
}
}
}
// Run is for running a command within a specific runtime (shell) and
// performs completion if completion context is detected. Otherwise, it
// executes the leaf Cmd returned from Seek calling its Method, and then
// Exits. Normally, Run is called from within main() to convert the Cmd
// into an actual executable program and normally it exits the program.
// Exiting can be controlled, however, with ExitOn/ExitOff when testing
// or for other purposes requiring multiple Run calls. Using Call
// instead will also just call the Cmd's Call Method without exiting.
// Note: Only bash runtime ("COMP_LINE") is currently supported, but
// others such a zsh and shell-less REPLs are planned.
func (x *Cmd) Run() {
x.cacheAliases()
// bash completion context
line := os.Getenv("COMP_LINE")
if line != "" {
cmd, args := x.Seek(ArgsFrom(line)[1:])
if cmd.Completer == nil {
list := comp.Standard(cmd, args...)
loop.Println(list)
Exit()
}
loop.Println(cmd.Completer(cmd, args...))
Exit()
}
// seek should never fail to return something, but ...
cmd, args := x.Seek(os.Args[1:])
if cmd == nil {
ExitError(x.UsageError())
}
// default to first Command if no Call defined
if cmd.Call == nil {
if cmd.Commands != nil {
def := cmd.Commands[0]
if def.Call == nil {
ExitError("default command \"%v\" must be callable", def.Name)
}
if err := def.Call(x, args...); err != nil {
ExitError(err)
}
Exit()
}
ExitError(x.UsageError())
}
// delegate
if err := cmd.Call(x, args...); err != nil {
ExitError(err)
}
Exit()
}
// UsageError returns an error with a single-line usage string.
func (x *Cmd) UsageError() error {
return fmt.Errorf("usage: %v %v\n", x.Name, x.Usage)
}
// Add creates a new Cmd and sets the name and aliases and adds to
// Commands returning a reference to the new Cmd. The name must be
// first.
func (x *Cmd) Add(name string, aliases ...string) *Cmd {
c := &Cmd{
Name: name,
Aliases: aliases,
}
x.Commands = append(x.Commands, c)
return c
}
// Cmd looks up a given Command by name or name from Aliases.
func (x *Cmd) Cmd(name string) *Cmd {
if x.Commands == nil {
return nil
}
for _, c := range x.Commands {
if name == c.Name {
return c
}
}
if c, has := x._aliases[name]; has {
return c
}
return nil
}
func (x *Cmd) CmdNames() []string {
list := []string{}
for _, c := range x.Commands {
if c.Name == "" {
continue
}
list = append(list, c.Name)
}
return list
}
// Param returns Param matching name if found, empty string if not.
func (x *Cmd) Param(p string) string {
if x.Params == nil {
return ""
}
for _, c := range x.Params {
if p == c {
return c
}
}
return ""
}
// IsHidden returns true if the specified name is in the list of
// Hidden commands.
func (x *Cmd) IsHidden(name string) bool {
if x.Hidden == nil {
return false
}
for _, h := range x.Hidden {
if h == name {
return true
}
}
return false
}
func (x *Cmd) Seek(args []string) (*Cmd, []string) {
if args == nil || x.Commands == nil {
return x, args
}
cur := x
n := 0
for ; n < len(args); n++ {
next := cur.Cmd(args[n])
if next == nil {
break
}
next.Caller = cur
cur = next
}
return cur, args[n:]
}
// ---------------------- comp.Command interface ----------------------
// mostly to overcome cyclical imports
// GetName fulfills the comp.Command interface.
func (x *Cmd) GetName() string { return x.Name }
// GetCommands fulfills the comp.Command interface.
func (x *Cmd) GetCommands() []string { return x.CmdNames() }
// GetHidden fulfills the comp.Command interface.
func (x *Cmd) GetHidden() []string { return x.Hidden }
// GetParams fulfills the comp.Command interface.
func (x *Cmd) GetParams() []string { return x.Params }
// GetOther fulfills the comp.Command interface.
func (x *Cmd) GetOther() map[string]string { return x.Other }
// GetCompleter fulfills the Command interface.
func (x *Cmd) GetCompleter() comp.Completer { return x.Completer }
// GetCaller fulfills the comp.Command interface.
func (x *Cmd) GetCaller() comp.Command { return x.Caller }