Files
gosh/gosh.go

226 lines
5.2 KiB
Go

package main
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/signal"
"path"
"plugin"
"regexp"
"strings"
"syscall"
"github.com/vladimirvivien/gosh/api"
)
var (
reCmd = regexp.MustCompile(`\S+`)
)
type Goshell struct {
ctx context.Context
pluginsDir string
commands map[string]api.Command
closed chan struct{}
}
// New returns a new shell
func New() *Goshell {
return &Goshell{
pluginsDir: api.PluginsDir,
commands: make(map[string]api.Command),
closed: make(chan struct{}),
}
}
// Init initializes the shell with the given context
func (gosh *Goshell) Init(ctx context.Context) error {
gosh.ctx = ctx
gosh.printSplash()
return gosh.loadCommands()
}
func (gosh *Goshell) loadCommands() error {
if _, err := os.Stat(gosh.pluginsDir); err != nil {
return err
}
plugins, err := listFiles(gosh.pluginsDir, `.*_command.so`)
if err != nil {
return err
}
for _, cmdPlugin := range plugins {
plug, err := plugin.Open(path.Join(gosh.pluginsDir, cmdPlugin.Name()))
if err != nil {
fmt.Printf("failed to open plugin %s: %v\n", cmdPlugin.Name(), err)
continue
}
cmdSymbol, err := plug.Lookup(api.CmdSymbolName)
if err != nil {
fmt.Printf("plugin %s does not export symbol \"%s\"\n",
cmdPlugin.Name(), api.CmdSymbolName)
continue
}
commands, ok := cmdSymbol.(api.Commands)
if !ok {
fmt.Printf("Symbol %s (from %s) does not implement Commands interface\n",
api.CmdSymbolName, cmdPlugin.Name())
continue
}
if err := commands.Init(gosh.ctx); err != nil {
fmt.Printf("%s initialization failed: %v\n", cmdPlugin.Name(), err)
continue
}
for name, cmd := range commands.Registry() {
gosh.commands[name] = cmd
}
gosh.ctx = context.WithValue(gosh.ctx, "gosh.commands", gosh.commands)
}
return nil
}
// TODO delegate splash to a plugin
func (gosh *Goshell) printSplash() {
fmt.Println(`
888
888
888
.d88b. .d88b. .d8888b 88888b.
d88P"88bd88""88b88K 888 "88b
888 888888 888"Y8888b.888 888
Y88b 888Y88..88P X88888 888
"Y88888 "Y88P" 88888P'888 888
888
Y8b d88P
"Y88P"
`)
}
// Open opens the shell for the given reader
func (gosh *Goshell) Open(r *bufio.Reader) {
loopCtx := gosh.ctx
line := make(chan string)
for {
// start a goroutine to get input from the user
go func(ctx context.Context, input chan<- string) {
for {
// TODO: future enhancement is to capture input key by key
// to give command granular notification of key events.
// This could be used to implement command autocompletion.
fmt.Fprintf(ctx.Value("gosh.stdout").(io.Writer), "%s ", api.GetPrompt(loopCtx))
line, err := r.ReadString('\n')
if err != nil {
fmt.Fprintf(ctx.Value("gosh.stderr").(io.Writer), "%v\n", err)
continue
}
input <- line
return
}
}(loopCtx, line)
// wait for input or cancel
select {
case <-gosh.ctx.Done():
close(gosh.closed)
return
case input := <-line:
var err error
loopCtx, err = gosh.handle(loopCtx, input)
if err != nil {
fmt.Fprintf(loopCtx.Value("gosh.stderr").(io.Writer), "%v\n", err)
}
}
}
}
// Closed returns a channel that closes when the shell has closed
func (gosh *Goshell) Closed() <-chan struct{} {
return gosh.closed
}
func (gosh *Goshell) handle(ctx context.Context, cmdLine string) (context.Context, error) {
line := strings.TrimSpace(cmdLine)
if line == "" {
return ctx, nil
}
args := reCmd.FindAllString(line, -1)
if args != nil {
cmdName := args[0]
cmd, ok := gosh.commands[cmdName]
if !ok {
return ctx, errors.New(fmt.Sprintf("command not found: %s", cmdName))
}
return cmd.Exec(ctx, args)
}
return ctx, errors.New(fmt.Sprintf("unable to parse command line: %s", line))
}
func listFiles(dir, pattern string) ([]os.FileInfo, error) {
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
filteredFiles := []os.FileInfo{}
for _, file := range files {
if file.IsDir() {
continue
}
matched, err := regexp.MatchString(pattern, file.Name())
if err != nil {
return nil, err
}
if matched {
filteredFiles = append(filteredFiles, file)
}
}
return filteredFiles, nil
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx = context.WithValue(ctx, "gosh.prompt", api.DefaultPrompt)
ctx = context.WithValue(ctx, "gosh.stdout", os.Stdout)
ctx = context.WithValue(ctx, "gosh.stderr", os.Stderr)
ctx = context.WithValue(ctx, "gosh.stdin", os.Stdin)
shell := New()
if err := shell.Init(ctx); err != nil {
fmt.Print("\n\nfailed to initialize:", err)
os.Exit(1)
}
// prompt for help
cmdCount := len(shell.commands)
if cmdCount > 0 {
if _, ok := shell.commands["help"]; ok {
fmt.Printf("\nLoaded %d command(s)...", cmdCount)
fmt.Println("\nType help for available commands")
fmt.Print("\n")
}
} else {
fmt.Print("\n\nNo commands found")
}
go shell.Open(bufio.NewReader(os.Stdin))
sigs := make(chan os.Signal)
signal.Notify(sigs, syscall.SIGINT)
select {
case <-sigs:
cancel()
<-shell.Closed()
case <-shell.Closed():
}
}